Thinking in Java 学习笔记

一、目录

  1. java基础数据类型
  2. 假设一个值是29.7,我们把它强转成一个char,那么结果值到底是 30 还是29 呢?
  3. 构造函数
  4. 垃圾收集
  5. 成员初始化
  6. public, protected, default(friendly), private 对应的访问权限
  7. final
  8. 方法过载与覆盖
  9. 抽象方法
  10. 接口
  11. 多重继承
  12. 多态
  13. 继承和 finalize()
  14. 构建器内部的多形性方法的行为
  15. 集合
  16. java的I/O
  17. 线程

二、内容

1、java基础数据类型

      java基础数据类型:int, float, double, boolean, short, long, byte以及char.
      其中,char类型是一个单一的 16 位 Unicode 字符。char类型用单引号表示也可以是整数。

1
2
3
4
cahr a = 'a'; // 必须限定长度为一个字节。等效于 char a = 97; 因为'a'在ASCII码表里对应97.
char b = '中文';
char c = 97;
System.out.println(a == c); // true

2、假设一个值是29.7,我们把它强转成一个char,那么结果值到底是 30 还是29 呢?

      将一个 float 或 double 值造型成整数值后,总是将小数部分“砍掉”,不作任何进位处理。所以结果是29.

3、构造函数

返回目录
      构造函数是在产生对象时被java系统自动调用的,我们不能在程序中像调用其他方法一样去调用构造方法(必须通过关键词new自动调用它)。
      构造器属于一种较特殊的方法类型,因为它没有返回值。
注意!

  • 构造器里通过this调另一个构造器,只能调用一个,不能调用两个。
  • 除构造器之外的方法不能调用构造器。

4、垃圾收集

返回目录

  • 垃圾收集并不等于“破坏”!
  • 我们的对象可能不会当作垃圾被收掉!
  • 垃圾收集只跟内存有关!

不必过多地使用finalize()。它并不是进行普通清除工作的理想场所。finalize()最有用处的地方之一是观察垃圾收集的过程。

5、成员初始化

返回目录
      Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码:

1
2
3
4
void f() {
int i;
i++;
}

上述代码会报错,提示未初始化。但是这样改:

1
2
3
4
5
6
7
public class InitialValues {
private int i;
public static void main(String[] args) {
InitialValues initialValues = new InitialValues();
System.out.println(initialValues.i); // 0
}
}

能正常运行且i有一个初始值0.

  • 初始化顺序
    在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。
    初始化的顺序是首先static(如果它们尚未由前一次对象创建过程初始化),接着是非static 对象,然后再构造器。

  • 明确进行的静态初始化
    static关键字定义的变量以及代码块在类初始化的时候就执行了,属于静态资源。
    这里一开始我有一个误区:我一开始以为只要实例化一个对象就把该类初始化一遍,初始化一遍是不是也就把static关键字定义的内容也初始化一次。
    事实上,这种理解是错误的。static修饰的内容在类第一次初始化后就当做静态资源暂存在java虚拟机上了,以后不管类初始化几次,都不会再对static修饰的内容初始化。做个比喻:一个类第一次初始化后就好比王者荣耀英雄刚进战场,把static修饰的内容赋为它的被动属性,被动属性英雄出场自带,其他属性升级(类被实例化)才能学到。
    所以下方代码执行后,明明实例化了两次Cups,但却只打印一次Cup(1) Cup(2)。因为第一次初始化后,static修饰的内容就不会再度执行了。

看下面代码对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.c1.f(99);
}
static Cups x = new Cups();
static Cups y = new Cups();
}
class Cup {
public Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup c1;
static Cup c2;
static {
c1 = new Cup(1);
c2 = new Cup(2);
}
public Cups() {
System.out.println("Cups()");
}
}

上述代码最终打印如下:

1
2
3
4
5
6
Cup(1)
Cup(2)
Cups()
Cups()
Inside main()
f(99)

但是如果把Cups类里面的static代码块前的static去掉,打印如下:

1
2
3
4
5
6
7
8
Cup(1)
Cup(2)
Cups()
Cup(1)
Cup(2)
Cups()
Inside main()
f(99)

如果不进行Cups的实例化呢?
直接把static Cups x = new Cups(); static Cups y = new Cups();注释掉,会发现打印输出:

1
2
3
4
Inside main()
Cup(1)
Cup(2)
f(99)

初始化一个类并不仅仅靠实例化,通过 类.属性名 或者 类.方法也可以对类进行初始化!

6、public, protected, default(friendly), private 对应的访问权限

返回目录
                  类本身 同一包内 子孙类 不同包
public                                          

protected                                    ×

default                                ×         ×

private                    ×            ×         ×

7、final

返回目录

  • final修饰的变量不能被重新赋值修改。
  • final修饰的方法不能被继承类修改。
  • final修饰的变量在定义时可以没有初始值,但是必须在构造器里赋值。
  • final修饰的类不能被继承。
  • final可以有效的关闭“动态绑定”。

8、方法过载与覆盖

返回目录

“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。

过载的例子:

1
2
3
4
5
6
7
8
9
class A {
void play(int i) {}
}
class C {
C() {}
}
class B extends A {
void play(C c) {}
}

      上面的例子中,B继承了A,A里面有方法play不过参数是int型的,而B里面的play方法参数却是C类型的,这里并不是覆盖A类里面的play方法,而是新增了一个使用C类型参数的play方法。

9、抽象方法

返回目录
语法:abstract void X();
它属于一种不完整的方法,只含有一个声明,没有方法主体。
包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。否则,编译器会向我们报告一条出错消息。

当继承抽象类时,子类必须将抽象类的抽象方法全部显式的写出来,否则报错。

10、接口

返回目录
      可以将接口视为一个“纯抽象”类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static 和final。接口只提供一种形式,并不提供实施的细节。

11、多重继承

返回目录
      由于接口根本没有具体的实施细节——也就是说,没有存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“x 从属于 a,也从属于 b,也从属于 c”。
      在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于 implements 关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.*;
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
public class Adventure {
static void t(CanFight x) { x.fight(); }
static void u(CanSwim x) { x.swim(); }
static void v(CanFly x) { x.fly(); }
static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero i = new Hero();
t(i); // Treat it as a CanFight
u(i); // Treat it as a CanSwim
v(i); // Treat it as a CanFly
w(i); // Treat it as an ActionCharacter
}
}

12、多态

返回目录

三个必要条件:继承重写父类引用指向子类对象
实现多态的技术称为动态绑定。是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

以下内容来自知乎!
直观的理解:
同一个方法名,但是参数不同,这叫方法重载。Overload

1
2
void foo(String str);
void foo(int num);

父类与子类有同样的方法名和参数,这叫方法覆盖。Override

1
2
3
4
5
6
7
8
9
10
class Parent {
void foo() {
System.out.println("Parent foo()");
}
}
class Child extends Parent {
void foo() {
System.out.println("Child foo()");
}
}

父类引用指向子类对象,调用方法时会调用子类的实现,而不是父类的实现,这叫多态。

1
2
Parent instance = new Child();
instance.foo(); // 此处其实调用的是 Child 类里面的 foo()

拓展深入理解下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Animal {
int num = 10;
static int age = 20;
public void eat() {
System.out.println("动物吃饭");
}
public static void sleep() {
System.out.println("动物在睡觉");
}
public void run() {
System.out.println("动物在奔跑");
}
}
class Cat extends Animal {
int num = 80;
static int age = 90;
String name = "tomCat";
public void eat() {
System.out.println("猫吃饭");
}
public static void sleep() {
System.out.println("猫在睡觉");
}
public void catchMouse() {
System.out.println("猫在抓老鼠");
}
}

测试类Demo_Test1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Demo_Test1 {
public static void main(String[] args) {
Animal am = new Cat();
am.eat();
am.sleep();
am.run();
// am.catchMouse(); 报错,Animal类里没有这个方法。
// System.out.println(am.name); 报错
System.out.println(am.num);
System.out.println(am.age);
}
}
// 打印输出
猫吃饭
动物在睡觉
动物在奔跑
10
20

由上述例子可以看出:

  • 子类 Cat 中重写或者覆盖了父类 Animal 中的 eat(),所以打印输出了 猫吃饭。
  • 子类 Cat 中虽然也重新对父类Animal中的 sleep() 进行重写,但是输出发现并未覆盖掉父类的方法。仔细观察发现,该方法是静态的,不会被覆盖
  • 子类 Cat 中虽然也重新对父类Animal中的 numage 进行赋值期望覆盖父类,但是由于age也是静态的,依旧不能被覆盖。不过需要着重关注的是num,该变量不是静态的,也没有用final修饰,却依然未被覆盖
  • 上述代码中有两处报错,分别是子类定义的name变量以及catchMouse()
    对这个现象的理解应该是,am声明时是Animal类型,虽然给它赋值了Cat类型并没有报错,但那是因为Cat类型继承自Animal类型,所以可以赋值。不过,am的本质依然是Animal类型而不是Cat类型,Animal类型中并没有name变量以及catchMouse(),所以会报错

那么可以总结出多态成员访问的特点:
成员变量
编译看左边(父类),运行也看左边(父类)。
成员方法
编译看左边(父类),运行却看右边(子类)。动态绑定。
静态方法
编译看左边(父类),运行也看左边(父类)。
只有非静态成员的方法,编译才看左边,运行看右边。

从上述例子可以看出,多态有一个弊端即无法访问子类定义的方法和变量。
怎么才能访问子类的方法和变量呢?
强转

1
2
3
Cat ct = (Cat)am;
ct.catchMouse();
System.out.println(ct.name);

13、继承和 finalize()

返回目录
看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.lizz.finalize;
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag) {
try {
super.finalize();
} catch (Throwable e) {
}
}
}
public static void main(String[] args) {
if(args.length != 0 && args[0].equals("finalize")) {
DoBaseFinalization.flag = true;
} else {
System.out.println("not finalizing bases");
}
new Frog();
System.out.println("bye!");
System.runFinalizersOnExit(true);
}
}
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println("Creating Character " + s);
}
protected void finalize() {
System.out.println("finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p = new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println("LivingCreature finalize");
if(DoBaseFinalization.flag) {
try {
super.finalize();
} catch (Throwable e) {
}
}
}
}
class Animal extends LivingCreature {
Characteristic p = new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag) {
try {
super.finalize();
} catch (Throwable e) {
}
}
}
}
class Amphibian extends Animal {
Characteristic p = new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag) {
try {
super.finalize();
} catch (Throwable e) {
}
}
}
}
// 打印输出:
not finalizing bases
Creating Character is alive
LivingCreature()
Creating Character has heart
Animal()
Creating Character can live in water
Amphibian()
Frog()
bye!
finalizing Characteristic can live in water
finalizing Characteristic has heart
finalizing Characteristic is alive
Frog finalize

仔细观察代码和后4个输出,发现输出顺序是从最低级的子类不断往上一直到基础类,也就是说finalize()是从最下面的子类不断向上调用的。
按照与 C++ 中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它们清除(破坏)。

14、构建器内部的多形性方法的行为

返回目录

设计构建器时一个特别有效的规则是:
      用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final 属性的那些方法(也适用于private方法,它们自动具有final 属性)。

      若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。
      从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.lizz.polymorphism;
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int i) {
radius = i;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.drwa(), radius = " + radius);
}
}
// 打印输出:
Glyph() before draw()
RoundGlyph.drwa(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

仔细观察上述代码以及输出,发现Glyph初始化调用构造器时,其内部的draw()并没有定义方法主体但是却打印输出了RoundGlyph类内部重写的draw()方法。
初始化的实际过程:

  • 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
  • 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在 RoundGlyph 构建器调用之前),此时会发现 radius 的值为 0,这是由于步骤(1)造成的。
  • 按照原先声明的顺序调用成员初始化代码。
  • 调用衍生类构建器的主体。

15、集合

返回目录

      当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java 提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)
所有Java 集合类都能自动改变自身的大小

15.1、Vector

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.lizz.set;
import java.util.Vector;
public class CatsAndDogs {
public static void main(String[] args) {
Vector cats = new Vector();
for(int i = 0; i < 7; i++) {
cats.addElement(new Cat(i));
}
cats.addElement(new Dog(7));
for(int i = 0; i < cats.size(); i++) {
((Cat)cats.elementAt(i)).print();;
}
}
}
class Cat {
private int catNumber;
Cat(int i) {
catNumber = i;
}
void print() {
System.out.println("Cat #" + catNumber);
}
}
class Dog {
private int dogNumber;
Dog(int i) {
dogNumber = i;
}
void print() {
System.out.println("Dog #" + dogNumber);
}
}

      可以看出,Vector 的使用是非常简单的:先创建一个,再用 addElement()置入对象,以后用 elementAt()取得那些对象(注意 Vector 有一个 size()方法,可使我们知道已添加了多少个元素,以便防止误超边界,造成违例错误)。
      CatDog 类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承,就默认为从 Object 继承)。所以我们不仅能用 Vector 方法将 Cat 对象置入这个集合,也能添加 Dog 对象,同时不会在编译期和运行期得到任何出错提示。用Vector 方法elementAt()获取原本认为是Cat 的对象时,实际获得的是指向一个Object 的引用,必须将那个对象转型为Cat

15.2、迭代器 Iterator

返回目录
      Vector有一个缺陷:需要事先知道集合的准确类型,否则无法使用。乍看来,这一点似乎没什么关系。但假若最开始决定使用Vector,后来在程序中又决定(考虑执行效率的原因)改变成一个 List(属于 Java1.2 集合库的一部分),这时又该如何做呢?
      可利用“反复器”(Iterator)的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为反复器是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现反复器存在一些似乎很奇怪的限制。例如,有些反复器只能朝一个方向移动。
Java 的Enumeration(枚举,注释②)便是具有这些限制的一个反复器的例子。除下面这些外,不可再用它做其他任何事情:
(1) 用一个名为 elements()的方法要求集合为我们提供一个 Enumeration。我们首次调用它的 nextElement()时,这个Enumeration 会返回序列中的第一个元素。
(2) 用nextElement() 获得下一个对象。
(3) 用hasMoreElements()检查序列中是否还有更多的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.lizz.set;
import java.util.Enumeration;
import java.util.Vector;
public class CatsAndDogs2 {
public static void main(String[] args) {
Vector cats = new Vector();
for(int i = 0; i < 7; i++)
cats.addElement(new Cat2(i));
// Not a problem to add a dog to cats:
cats.addElement(new Dog2(7));
Enumeration e = cats.elements();
while(e.hasMoreElements())
((Cat2)e.nextElement()).print();
// Dog is detected only at run-time
}
}
class Cat2 {
private int catNumber;
Cat2(int i) {
catNumber = i;
}
void print() {
System.out.println("Cat number " +catNumber);
}
}
class Dog2 {
private int dogNumber;
Dog2(int i) {
dogNumber = i;
}
void print() {
System.out.println("Dog number " +dogNumber);
}
}

15.3、BitSet

返回目录
      BitSet 实际是由“二进制位”构成的一个 Vector。如果希望高效率地保存大量“开-关”信息,就应使用BitSet。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型的数组慢一些。
      此外,BitSet 的最小长度是一个长整数(Long)的长度:64 位。这意味着假如我们准备保存比这更小的数据,如 8 位数据,那么 BitSet 就显得浪费了。所以最好创建自己的类,用它容纳自己的标志位。

15.4、Stack

返回目录
      Stack 有时也可以称为“后入先出”(LIFO)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有 Java 集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。
      Stack本身通过扩展Vector而来,而Vector本身是一个可增长的对象数组(a growable array of objects)那么这个数组的哪里作为Stack的栈顶,哪里作为Stack的栈底?
答案只能从源代码中寻找,jdk1.7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Stack<E> extends Vector<E> {
/**
* Creates an empty Stack.
*/
public Stack() {
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public boolean empty() {
return size() == 0;
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = 1224463164541339165L;
}

      以前就一直听说栈是后入先出,但只停留于理论记忆,其实根本不明白这是什么意思。现在看了Stack的源码实现就有点豁然开朗了。内部的push()以及pop()跟js太像了,我估摸着后入先出的实现就是靠着pop(),该方法默认删除数组最后一个元素,而元素又是通过push()放进去的,默认是放在数组最后一个,所以一直都说栈是后入先出
      栈顶其实就是数组最后一项。
      注意!!!Stack并不要求其中保存数据的唯一性,当Stack中有多个相同的item时,调用search方法,只返回与查找对象equal并且离栈顶最近的item与栈顶间距离(见源码中search方法说明)。

15.5、Hashtable

返回目录
      Vector 允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如需要通过一个对象来查找呢?
      在Java 中,这个概念具体反映到抽象类 Dictionary 身上。该类的接口是非常直观的 size()告诉我们其中包含了多少元素;isEmpty()判断是否包含了元素(是则为 true);put(Object key, Object value)添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get(Object key)获得与某个键对应的值;而remove(Object Key)用于从列表中删除“键-值”对。还可以使用枚举技术:keys()产生对键的一个枚举(Enumeration);而 elements() 产生对所有值的一个枚举。这便是一个Dictionary(字典)的全部。
      Dictionary 的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个 Vector,一个用于容纳键,另一个用来容纳值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.lizz.set;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Vector;
public class AssocArray extends Dictionary {
private Vector keys = new Vector();
private Vector values = new Vector();
public int size(){
return keys.size();
}
public boolean isEmpty() {
return keys.isEmpty();
}
public Object put(Object key, Object value) {
keys.addElement(key);
values.addElement(value);
return key;
}
public Object get(Object key) {
int index = keys.indexOf(key);
if(index == -1) {
return null;
}
return values.elementAt(index);
}
public Object remove(Object key) {
int index = keys.indexOf(key);
if(index == -1) {
return null;
}
keys.removeElementAt(index);
Object returnvalue = values.elementAt(index);
values.removeElementAt(index);
return returnvalue;
}
public Enumeration keys() {
return keys.elements();
}
public Enumeration elements() {
return values.elements();
}
public static void main(String[] args) {
AssocArray aa = new AssocArray();
for(char c = 'a'; c < 'z'; c++) {
aa.put(String.valueOf(c), String.valueOf(c).toUpperCase());
}
char[] ca = {'a', 'e', 'i', 'o', 'u'};
for(int i = 0; i < ca.length; i++) {
System.out.println("Uppercase: " + aa.get(String.valueOf(ca[i])));
}
}
}

一个需要注意的示例
SpringDetector.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.lizz.set;
import java.util.Hashtable;
public class SpringDetector {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
for(int i = 0; i < 10; i++) {
hashtable.put(new Groundhog(i), new Prediction());
}
System.out.println("hashtable = " + hashtable + "\n");
System.out.println("Looking up prediction for groundhog #3:");
Groundhog groundhog = new Groundhog(3);
/*
* Groundhog 是从通用的 Object 根类继承的(若当初未指定基础类,则所有类最终都是从Object 继承的)。
* 事实上是用 Object 的 hashCode()方法生成每个对象的散列码,而且默认情况下只使用它的对象的地址。
* 所以,Groundhog(3)的第一个实例并不会产生与Groundhog(3)第二个实例相等的散列码,而我们用第二个实例进行检索。
* 大家或许认为此时要做的全部事情就是正确地覆盖 hashCode()。但这样做依然行不能,除非再做另一件事情:覆盖也属于Object 一部分的 equals()。
* 当散列表试图判断我们的键是否等于表内的某个键时,就会用到这个方法。同样地,默认的 Object.equals()只是简单地比较对象地址,
* 所以一个 Groundhog(3)并不等于另一个 Groundhog(3)。因此,为了在散列表中将自己的类作为键使用,必须同时覆盖 hashCode()和equals(),
* 看示例 SpringDetector2
*/
if(hashtable.containsKey(groundhog)) {
System.out.println((Prediction)hashtable.get(groundhog));
}
}
}
class Groundhog {
int ghNumber;
Groundhog(int n) {
ghNumber = n;
}
}
class Prediction {
boolean shadow = Math.random() > 0.5;
public String toString() {
if(shadow)
return "Six more weeks of Winter!";
else
return "Early Spring!";
}
}

SpringDetector2.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.lizz.set;
import java.util.Hashtable;
public class SpringDetector2 {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
for(int i = 0; i < 10; i++) {
hashtable.put(new Groundhog2(i), new Prediction());
}
System.out.println("hashtable = " + hashtable + "\n");
System.out.println("Looking up prediction for groundhog #3:");
Groundhog2 groundhog2 = new Groundhog2(3);
if(hashtable.containsKey(groundhog2)) {
System.out.println((Prediction)hashtable.get(groundhog2));
}
}
}
class Groundhog2 {
int ghNumber;
Groundhog2(int n) {
ghNumber = n;
}
public int hashCode() {
return ghNumber;
}
public boolean equals(Object o) {
return (o instanceof Groundhog2) && (ghNumber == ((Groundhog2)o).ghNumber);
}
}

15.6、排序

返回目录
      书中的例子主要用到了String.class里面的compareTo()方法。那就看看源码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final char value[];
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}

      compareTo()接受一个String类型参数,由于涉及到了强转相当于new String("参数")调用了String的带String类型参数的构造器,这样value就有值了。而value初始化是一个char类型的数组,所以具有length属性。接下来比较调用compareTo()String与传入的String参数长度并取较小的值。然后利用while()循环从数组下标为0开始进行比较,如果全部相等返回长度的差值0,如果数组元素中有一个不相等,则返回相应的ASCII码相减后的值(第一个参数大于第二个那就返回正整数,小于的话返回负整数)。

15.7、新集合

返回目录
新的集合库考虑到了“容纳自己对象”的问题,并将其分割成两个明确的概念:
(1) 集合(Collection):一组单独的元素,通常应用了某种规则。在这里,一个 List(列表)必须按特定的顺序容纳元素,而一个Set(集)不可包含任何重复的元素。相反,“包”(Bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。
(2) 映射(Map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙。这进一步证明了应该分离成单独的概念。另一方面,可以方便地查看 Map 的某个部分。只需创建一个集合,然后用它表示那一部分即可。这样一来,Map 就可以返回自己键的一个Set、一个包含自己值的List 或者包含自己“键-值”对的一个List。和数组相似,Map 可方便扩充到多个“维”,毋需涉及任何新概念。只需简单地在一个Map 里包含其他 Map(后者又可以包含更多的Map,以此类推)。

      这张图刚开始的时候可能让人有点儿摸不着头脑,但在通读了本章以后,相信大家会真正理解它实际只有三个集合组件:Map,List 和 Set
      虚线框代表“抽象”类,点线框代表“接口”,而实线框代表普通(实际)类。点线箭头表示一个特定的类准备实现一个接口(在抽象类的情况下,则是“部分”实现一个接口)。双线箭头表示一个类可生成箭头指向的那个类的对象。例如,任何集合都可以生成一个反复器(Iterator),而一个列表可以生成一个ListIterator(以及原始的反复器,因为列表是从集合继承的)。
      在观看这张示意图时,真正需要关心的只有位于最顶部的“接口”以及普通(实际)类——均用实线方框包围。通常需要生成实际类的一个对象,将其上溯造型为对应的接口。以后即可在代码的任何地方使用那个接口。
简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.lizz.set;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class SimpleCollection {
public static void main(String[] args) {
Collection c = new ArrayList();
for(int i = 0; i < 10; i++) {
c.add(Integer.toString(i));
}
Iterator it = c.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}

      查了下源码,发现Collection接口定义了add()以及iterator()等方法,而ArrayList实现了List接口,List接口继承自Collection,其实也就是ArrayList变相的实现了Collection接口里的方法。

      上述代码while循环用到了hasNext(),Iterator是一个接口,我查了下ArrayList以及Collection并没有明确继承或实现该接口,然后在Collection里的确找到了iterator()方法,然后又在ArrayList里找到了该方法的实现:

1
2
3
4
5
6
7
8
9
10
11
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
...

      由源码可以看出,iterator()返回一个Itr类的实例,而该类实现了Iterator接口。在该类内部,它重写了或者说覆盖实现了hasNext()

15.7.1、removeAll()以及retainAll()

返回目录

下面给出Thinking in Java示例中的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Collection1 {
public static Collection fill(Collection c, int start, int size) {
for(int i = start; i < start + size; i++) {
c.add(Integer.toString(i));
}
return c;
}
public static Collection fill(Collection c) {
return fill(c, 0, 10);
}
public static Collection newCollection() {
return fill(new ArrayList());
}
public static Collection newCollection(int start, int size) {
return fill(new ArrayList(), start, size);
}
public static void print(Collection c) {
for(Iterator x = c.iterator(); x.hasNext();) {
System.out.println(x.next() + " ");
}
System.out.println();
}
public static void main(String[] args) {
Collection c = newCollection();
c.add("ten");
c.add("eleven");
c.addAll(newCollection());
print(c);
Collection c2 = newCollection(5, 3);
c.retainAll(c2);
print(c);
c.removeAll(c2);
}
}

      上述代码我在本地打印输出到了c.retainAll(c2);这一步有点不懂了。先是点进去查看了ArrayList.class里面的retainAll()方法的实现,发现它返回的是batchRemove()方法,看了下batchRemove(),一开始看不下去就去百度了。百度得知retainAll()是用来取交集的,结合控制台输出我知道这种解释是对的,但是百度还来了句removeAll()也调用了batchRemove()方法,这我就有点懵了。我就好奇他是怎么实现的呢?
      下面贴出相关的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
protected transient int modCount = 0; // 这里是继承自AbstractList,并不是ArrayList内定义的
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

      研究batchRemove()内部的实现首先要知道elementData以及size,所以我把这两个变量初始化以及赋值相关的方法也贴出来了。
      结合示例,Collection c = newCollection();这一步调用了newCollection()方法,而该方法内部返回了fill(new ArrayList()),我们知道ArrayList()其实实现了Collection接口,所以这一步调用的是public static Collection fill(Collection c)这个对应的fill方法。该方法内部又返回了fill(c, 0, 10),这里其实是调用public static Collection fill(Collection c, int start, int size)这个对应的方法,然后利用该方法对c进行add元素并返回c
      接下来针对每一步进行详解:

  1.       其实示例中一开始的Collection c = newCollection();实际相当于Collection c = new ArrayList(),这一步实例化了ArrayList,那么同样的会调用它的构造方法。在构造方法里,给elementData初始化赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空对象集合)。
  2.       历经几步调用终于到了正主public static Collection fill(Collection c, int start, int size),在该方法内使用了add()方法,也正是调用了add(),elementData以及size才开始赋值改变的。
  3.       add()内的第一步就调用了ensureCapacityInternal(size + 1),那么size从何而来呢?其实size在初始化时就定义了但是没有赋值,这里采用了默认值0。观察ensureCapacityInternal()方法,使用minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity)比较它们两个中较大的值并重新赋给minCapacity,DEFAULT_CAPACITY是常数设定为10比1大所以经过该方法后,minCapacity的值被改为10。随后又调用了ensureExplicitCapacity()方法。在ensureExplicitCapacity()里判断传过来的值是不是比elementData的长度大,是的话继续调用grow(minCapacity)
  4.       grow()方法是重点(容量增长算法)。摘抄自 https://www.cnblogs.com/aoguren/p/4765439.html

    • 先得到数组的旧容量,然后进行oldCapacity + (oldCapacity >> 1),将oldCapacity 右移一位,其效果相当于oldCapacity /2,我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍
    • 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量.
    • 再检查新容量是否超出了ArrayList所定义的最大容量,若超出了,则调用hugeCapacity()来比较minCapacityMAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为ArrayList定义的最大容量,否则,新容量大小则为 minCapacity。(在判断容量是否超过MAX_ARRAY_SIZE的值,MAX_ARRAY_SIZE值为Integer.MAX_VALUE - 8,比int的最大值小8,不知道为什设计,可能方便判断吧。如果已经超过,调用hugeCapacity方法检查容量的int值是不是已经溢出。)
    • 最后确定了新的容量,就使用Arrays.copyOf方法来生成新的数组,copyOf也已经完成了将就的数据拷贝到新数组的工作。
  5.       通过上面的方法,完成了elementData的重新赋值,接下来在看batchRemove()就容易很多了。
          结合示例,batchRemove()里面的c其实是我传进来的c2,循环遍历elementData,然后结合complement同时利用contains()来判断c中是否存在elementData中的元素,如果complement为true,同时c中也存在elementData中的元素,那么对elementData重新赋值(从下标为0开始)。最后通过finally判断w是不是和size相等,不等的话将elementData中多余的参数给清空掉。(w其实就是c的长度)

15.7.2、Lists

返回目录
包含:List、ArrayList、LinkedList
ArrayList 允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。
LinkedList 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用 ArrayList。

15.7.3、Sets

返回目录
包含:Set、HashSet、TreeSet
Set 添加到 Set 的每个元素都必须是独一无二的;否则Set 就不会添加重复的元素。添加到 Set 里的对象必须定义equals(),从而建立对象的唯一性。Set 拥有与 Collection 完全相同的接口。一个 Set不能保证自己可按任何特定的顺序维持自己的元素。
HashSet 用于除非常小的以外的所有Set。对象也必须定义 hashCode()
TreeSet 由一个“红黑树”后推得到的顺序 Set。

15.7.4、Maps

返回目录
包含:Map、HashMap、TreeMap
HashMap 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子”。https://www.cnblogs.com/skywang12345/p/3310835.html
TreeMap 在一个“红-黑”树的基础上实现。查看键或者“键-值”对时,它们会按固定的顺序排列(取决于Comparable 或Comparator,稍后即会讲到)。TreeMap 最大的好处就是我们得到的是已排好序的结果。

15.7.5、Arrays.binarySearch

返回目录
      排序与搜索里面举了一个例子,用到了sortbinarySearch方法。sort我重开了一篇博客去研究它的源码实现,接下来的binarySearch方法一开始我没在意,直到我仔细研读书上的文字,发现它强调了:

与 binarySearch()有关的还有一个重要的警告:若在执行一次binarySearch()之前不调用 sort(),便会发生不可预测的行为,其中甚至包括无限循环。

      这段话的意思无非就是使用binarySearch前必须先调用sort进行排序。这样就很有趣了,我便去看看它的源码实现:
Array.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int binarySearch(byte[] a, byte key) {
return binarySearch0(a, 0, a.length, key);
}
private static int binarySearch0(byte[] a, int fromIndex, int toIndex, byte key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
byte midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}

Array1.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Array1 {
static Random r = new Random();
static String ssource = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz";
static char[] src = ssource.toCharArray();
public static String randString(int length) {
char[] buf = new char[length];
int rnd;
for(int i = 0; i < length; i++) {
rnd = Math.abs(r.nextInt()) % src.length;
buf[i] = src[rnd];
}
return new String(buf);
}
public static String[] randStrings(int length, int size) {
String[] s = new String[size];
for(int i = 0; i < size; i++) {
s[i] = randString(length);
}
return s;
}
public static void print(byte[] b) {
for(int i = 0; i < b.length; i++) {
System.out.println(b[i] + " ");
}
System.out.println();
}
public static void print(String[] s) {
for(int i = 0; i < s.length; i++) {
System.out.print(s[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
byte[] b = new byte[15];
r.nextBytes(b);
print(b);
Arrays.sort(b);
print(b);
int loc = Arrays.binarySearch(b, b[10]);
System.out.println("Location of " + b[10] + " = " + loc);
String[] s = randStrings(4, 10);
print(s);
Arrays.sort(s);
print(s);
loc = Arrays.binarySearch(s, s[4]);
System.out.println("Location of " + s[4] + " = " + loc);
}
}

      源码中有(low + high) >>> 1这句代码,不明白的可以看:https://lizhongzhen11.github.io/2018/04/02/%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6%E8%AE%A1%E7%AE%97%E5%B9%B3%E5%9D%87%E5%80%BC/
      源码其实很简单,就是取下标平均值得到位于数组中间的值,然后拿该值和传进来的值比较,不断地去缩短范围,最终得到数组下标。但是,这种方法必须有一个前提:该数组是从小到大排列好的,因为它不断地用中间下标对应的值与传入的值进行比较,它认为,如果中间值比传入的小或者大,那么通过不断地缩短开始和结束下标范围就一定能找到传入值,这一切的前提都必须是排序好的。

16、java的I/O

返回目录
抄自:http://www.importnew.com/23708.html
      Java中I/O操作主要是指使用Java进行输入、输出操作。Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列。数据流是一组有序、有起点和终点的字节的数据序列。包括输入流和输出流。

      流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:
1) 字节流:数据流中最小的数据单元是字节
2) 字符流:数据流中最小的数据单元是字符,Java中的字符是Unicode编码,一个字符占用两个字节。

      Java.io包中最重要的就是5个类和一个接口。5个类指的是FileOutputStreamInputStreamWriterReader;一个接口指的是Serializable

      Java I/O主要包括如下3层次:

  1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等
  2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类
  3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

      主要类如下:

  1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
  2. InputStream(字节流,二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  3. OutputStream(字节流,二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
  4. Reader(字符流,文本格式操作):抽象类,基于字符的输入操作。
  5. Writer(字符流,文本格式操作):抽象类,基于字符的输出操作。
  6. RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。


17、线程

返回目录
http://www.cnblogs.com/xrq730/p/4850883.html
      TestThread.state里面定义了6种状态,分别是:

  1. NEW:线程刚创建,还未启动
  2. RUNNABLE:就绪状态。当调用了start()方法时就进入该状态。
  3. BLOCKED:如果某一线程正在等待监视器锁,以便进入一个同步的块/方法,那么这个线程的状态就是阻塞BLOCKED
  4. WAITING:某一线程因为调用不带超时的Objectwait()方法、不带超时的Threadjoin()方法、LockSupportpark()方法,就会处于等待WAITING状态
  5. TIMED_WAITING:某一线程因为调用带有指定正等待时间的Objectwait()方法、Threadjoin()方法、Threadsleep()方法、LockSupportparkNanos()方法、LockSupportparkUntil()方法,就会处于超时等待TIMED_WAITING状态
  6. TERMINATED:线程调用终止或者run()方法执行结束后,线程即处于终止状态。处于终止状态的线程不具备继续运行的能力
Newer Post

快速排序和冒泡排序

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;最近空闲时间逛论坛发现,好多人在面试时问了排序算法,比如快速排序以及冒泡排序。我回想了下,依稀记得有什么for循环嵌套,然后就没了。这样可不好,不能工作了就忘了这些基础,会被淘汰的。所以又重新学习了下这两个算法,发现自己还是有 …

继续阅读
Older Post

TCP三次握手与四次挥手

参考自:http://blog.csdn.net/qq598535550/article/details/52997218http://blog.csdn.net/guyuealian/article/details/52535294 声明&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; …

继续阅读