前言
前阵子刚看完《Java编程思想》这本书,写篇文章来记录一下知识点。
一切都是对象
1.对象存储到什么地方?
(1)寄存器
最快的存储区,数量有限。根据需求分配,无法直接控制
(2)堆栈
位于通用RAM(随机访问存储器)中。某些数据存储于堆栈中,特别是对象引用。速度仅次于寄存器
(3)堆
用于存放所有的Java对象。当执行new操作时,会自动在堆里进行存储分配
(4)常量存储
通常直接存放在程序代码内部
(5)非RAM存储
数据存活于程序之外,在程序没有运行时也可以存在。两个基本的例子是流对象和持久化
2.基本类型变量(int/double/long/.....)直接存储值并置于堆栈中,因此更加高效。
3.当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都被初始化为null。
4.当变量作为类的成员使用时,Java才确保给定其默认值。然而此方法并不适用于局部变量(定义于方法内的变量,局部变量不会被初始化)
5.main方法中的args用来存储命令行参数。
操作符
1.如果对对象使用a=b,那么a和b都指向原本只有b指向的那个对象。
2.==和!=比较的是对象的引用。
3.基本类型比较内容是否相等直接使用==和!=即可。
4.Object类的equals方法默认使用"=="比较。
5.大多数Java类库都重写了equals方法,以便用来比较对象的内容,而非比较对象的引用。
6.如果对char、byte或short类型数值移位处理,在进行移位之前会先被转换为int类型,并且得到的结果也是一个int类型的值。
初始化和清理
1.在Java中,"初始化"和"创建"被捆绑在一起,两者不能分离。
2.涉及基本类型的重载:
如果传入的数据类型(实参)小于方法中声明的形参,实际数据类型就会被提升。
char型有所不同,如果无法找到恰好接受char类型参数的方法,就会把char直接提升至int。
如果传入的实际参数较大,就会强制类型转换。
3.如果已经定义了一个构造器,编译器就不会帮你自动创建默认构造器。构造器实际上是static方法,只不过该static声明是隐式的
4.this关键字只能在方法内部调用,表示对"调用方法的那个对象"的引用。
5.在构造器中可通过this调用另一个构造器。但却不能调用两个构造器。此外,必须将构造器调用置于最起始处,否则会报错。
6.在static方法的内部不能调用非静态方法,反过来是可以的。
7.垃圾回收器只知道回收那些经由new分配的内存。
8.native(本地方法)是一种在Java中调用非Java代码的方式。
9.在类的内部,变量定义的先后顺序决定了初始化的顺序。
即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
10.无论创建多少个对象,静态数据都只占一份存储区域。
11.静态初始化只有在必要时刻进行。只有在"第一个对象被创建"
或者"第一次访问静态数据"
的时候,它们才会被初始化。此后静态对象不会再被初始化。
12.初始化的顺序是先静态对象(如果它们还没被初始化过),而后是非静态对象。
复用类
1.每一个非基本的对象都有一个toString方法。
2.Java会自动在子类的构造器中插入对父类构造器的调用。所有构造器都会显示或隐式地调用父类构造器
3.构建过程是从父类"向外"扩散的,所以父类在子类构造器可以访问它之前就已经完成了初始化。
4.调用父类构造器是你在子类构造器中要做的第一件事。
5.向上转型:可以理解为"子类是父类的一种类型"。
6.对于基本类型,final使数值恒定不变。对于对象引用,final使引用恒定不变(即无法再把它指向另一个对象)。
7.final类:防止别人继承。
final参数:你可以读参数,但却无法修改参数。(一般用来向匿名内部类传递数据)
final方法:把方法锁定,以防任何继承类修改它的含义。
多态
1.把对某个对象的引用视为对其父类型的引用的做法被称为向上转型。
2.只有非private方法才能被覆盖。
3.域和静态方法不是多态的。(当子类转型为父类引用时,任何域访问操作都将由编译器解析,所以不是多态的)
4.调用构造器要遵循下面的顺序:
(1)调用父类构造器。
(2)按声明顺序调用成员的初始化方法。
(3)调用子类构造器。
5.为什么在子类构造器里要先调用父类构造器???
答:在构造器内部,我们必须要确保要使用的成员都已经构建完毕。为确保这一目的,唯一的方法就是首先调用父类构造器。那么在进入子类构造器时,在父类中可供我们访问的成员都已得到初始化。
6.继承与清理
(1)当覆盖父类的dispose()方法时,务必记住调用父类版本的dispose()方法,否则父类的清理动作就不会发生。
(2)应该首先对子类进行清理,然后才是父类。这是因为子类的清理可能会调用父类的某些方法,因此不该过早地销毁它们。
7.构造器内部的多态行为
Class A{
void draw(){print("A.draw()");}
A(){draw();}
}
Class B extends A{
void draw(){print("B.draw()");}
}
Class Test{
public static void main(String[] args){
new B();
}
}
输出:B.draw()
在B构造器中调用父类A构造器,在里面调用的是子类B覆盖后的draw()方法。
因此,编写构造器时有一条有效的准则:"如果可以的话,避免在构造器内部调用其它方法"。
接口
1.包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。
2.如果继承抽象类,那么必须为基类中所有的抽象方法提供方法定义。如果不这样做,那么子类就必须限定为抽象类。
3.接口中的域隐式地是static和final的。
4.接口中的方法默认是public的,因此实现接口中的方法时必须定义为public的。否则其访问权限就降低了,这是Java不允许的。
5.通过继承来扩展接口
interface A{....}
interface B{....}
interface C extends A,B{.....}//仅适用于接口继承
一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。
6.嵌套在类中的接口可以是private的。
嵌套在另一个接口中的接口自动是public,而不能声明为private的。
7.当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被实现。
内部类
1.当生成一个内部类的对象时,内部类对象隐式地保存了一个引用,指向创建它的外部类对象。(静态内部类除外)
内部类对象能够访问外部类对象的所有成员和方法(包括私有的)。
- .new和.this
class A{
class B{}
}
(1)创建内部类对象:
A a=new A();
A.B b=a.new B();
(2)生成外部类对象引用:
class A{
class B{
public A getA(){
return A.this;
}
}
}
3.如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。
4.当创建静态内部类的对象时:
(1)不需要其外部类对象
(2)不能从静态内部类对象中访问非静态的外部类对象
5.不管一个内部类被嵌套多少层,它都能够访问所有它嵌入的外部类的所有成员。
6.类文件命名规则:外部类名字+$+内部类名字
(1)普通内部类:A$B
(2)匿名内部类(编译器会生成一个数字作为其标识符) A$1
类型信息
1.如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),toString()方法会被自动调用,以生成该对象的String。
2.每当编写并且编译了一个新类,就会产生一个Class对象。更恰当地说,是被保存在一个同名的.class文件中。
3.Java程序在它开始运行之前并非完全被加载,其各个部分是在必需时才加载的。
类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件。
4.Class对象仅在需要的时候才被加载,static初始化块是在类加载时进行的。
5.Class.forName("A")
用来加载类A并获取A.Class对象。字符串必须使用全限定名,包括包名。
6.Class AClass=A.class
当使用.class来创建Class对象时,不会自动地初始化该Class对象。
7.static final int a=1;
static final int b=ClassInitialization.rand.nextInt(1000);
static int c=1;
如果一个static final值是编译期常量,就像a那样,那么这个值不需要对初始化类就可以被读取。
读取b和c则需要先对类进行初始化。
泛型
1.擦除
(1)可以声明ArryaList.class,但不能声明ArrayList
(2)
Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);//返回true
(3)List
2.擦除的补偿
public class czy{
public static void f(Object arg){
(1)if (arg instanceof T){} //错误
(2)T t = new T(); //错误
(3)T[] array = new T[10]; //错误
}
}
(1)无法使用instanceof是因为其类型信息已经被擦除
了。
(2)new T()失败,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认构造器。
除非引入类型标签
public boolean f(Class kind,Object arg){
t = kind.newInstance(); //正确
return kind.isInstance(arg); //正确
}
3. extends T>表示类型的上界,表示参数化类型可能是T或T的子类
4.如下
public void f(List super Apple> apples){
apples.add(new Apple()); //正确
apples.add(new GreenApple()); //正确 GreenApple继承自Apple
apples.add(new Fruit()); //错误
}
向apples其中添加Apple或Apple的子类型是安全的。
因为Apple或者GreenApple肯定是 super Apple>的子类,所以编译通过。
5.List>看起来等价于List
6.自动包装机制不能应用于数组。
7.public class Czy{
void f(List v){}
void f(List v){}//错误
}
由于擦除的原因,重载方法会产生相同的类型签名。
数组
1.你不能实例化具有参数化类型的数组:
Czy[] czy = new Czy[10];——>编译错误
擦除会移除参数类型信息,而数组必须知道它们所持有的确切类型,以强制保证类型安全。
2.复制数组
System.arraycopy()→用它复制数组比用for循环快得多。
System.arraycopy()不会执行自动包装和自动拆包,两个数组必须具有相同的确切类型。
3.数组比较
Arrays类提供了重载后的equals()方法,用来比较整个数组。
此方法针对所有基本类型和Object都做了重载。
4.数组排序
(1)实现Comparable接口,实现compareTo()方法。
如果没有实现Comparable接口,调用Arrays.sort()方法会抛出异常。因为sort方法需要把参数类型变为Conparable
(2)创建一个实现了Comparator接口的类。
容器深入研究
1.Arrays.asList()会生成一个固定尺寸的List,该List只支持那些不会改变数组大小的操作。
任何对底层数据结构的尺寸进行修改都会抛出一个异常。
应该把Arrays.asList()的结果作为构造器参数传递给Collection,这样就可以生成允许使用所有方法的普通容器。
比如:List
2.Collections.unmodifiableList()产生不可修改的容器。
3.散列码是"相对唯一"的,用以代表对象的int值。
4.如果不为你的键覆盖hashcode()和equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet,LinkedHashMap)就无法正确处理你的键。
5.LinkedHashMap在插入时比HashMap慢一点,因为它维护散列表数据结构的同时还要维护链表(以保持插入顺序)。
正是由于这个链表,使得其迭代速度更快。
6.HashMap使用的默认负载因子是0.75.
7.Java容器采用快速报错(fail-fast)机制。
它会检查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会抛出异常。防止在你迭代容器的时候,其它线程在修改容器的值。
8.WeakHashMap允许垃圾回收器自动清理键和值。
9.Iterable接口被foreach用来在序列中移动。如果你创建了任何实现Iterable接口的类,都可以将它用于foreach语句中。
foreach语句可以用于数组或其它任何Iterable,但这并不意味着数组也是一个Iterable。
10.总结
- 如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,就应该使用LinkedList。
- 各种Queue以及栈的行为,由LinkedList提供支持。
- HashMap用来快速访问。
TreeMap使得“键”始终处于排序状态,所以没有HashMap快。
LinkedHashMap保持元素插入的顺序。 - Set不接受重复元素。
HashSet提供最快的查询速度。
TreeSet保持元素处于排序状态。
LinkedHashSet以插入顺序保存元素。 - 新程序中不应该使用过时的Vector、Hashtable和Stack。