Java编程思想笔记
1. 前期、后期绑定 P9 P150
将一个方法调用同一个方法主体关联起来称为绑定。若在程序执行前(由编译器和连接程序实现)进行绑定叫前期绑定,例如面向过程语言C。
在运行时根据对象的类型进行绑定叫后期绑定。编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体并加以调用。
Java中除了static和final(private方法属于final方法)方法外,其他方法都是后期绑定。多态性通过动态绑定实现。
2. 单根继承好处 P11
3. 对于自己创建的类,equals()的默认行为是比较引用。所以除非在自己的类中覆盖equals()方法,否则不可能表现出我们希望的行为。 P45
4. Java没有sizeof。因为所有数据类型在所有机器中的大小是相同的。在C/C++中需要使用sizeof的最大原因是为了移植。 P56
5. 在构造器中调用构造器 P86
在构造器中可以用this调用另一个构造器,但不能调用2个。此外,必须将构造调用置于最起始处,否则编译器会报错。
除构造器外,编译器禁止在其他任何方法中调用构造器。
6. static
在static方法内部不能调用非静态方法,反之可以。
7. finalize() P88 或 《Effective Java》 P26
避免使用该方法,因为该方法不能保证被及时的执行。
该方法有两个用途,一是充当最后的安全网,例如迟一点释放关键资源总比永远不释放要好;
二是在本地对等体(native)并不拥有关键资源的前提下,当它的Java对等体被回收的时候,可以用该方法来释放该资源,例如用C/C++调用malloc()分配对象后,可以通过 finalize()用本地方法调用free()来释放内存。
8. 垃圾回收器 P90
9. 类的每个成员保证都会有一个初始值。方法内的局部变量则不会默认初始化。
变量的赋值顺序与方法定义的顺序无关,例如:
public class Test{ int i = f() int f() {return 1;} }
10. 初始化顺序 P96
- 假设有个名为Dog的类,即使没有显式地使用static关键字,构造器实际上也是静态方法。当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法或静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
- 然后载入Dog.class(这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
- 这块空间会被清零。
- 执行所有出现于(非静态)字段定义处的初始化动作。
- 执行构造器。(最后才执行)
11. Java会自动在导出类的构造器中插入对基类构造器(默认构造器或无参构造器)的调用。如果想调用带参数的基类构造器,需要super显式调用。
12. 代理是继承与组合之间的中庸之道。因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。
13. final
将方法中的参数指明为final,表明你无法在方法中更改参数引用所指向的对象。
void with(final Dog d){ d = new Cat(); // error }
将方法设为final,是想要确保在继承中使方法行为保持不变,并且不会被覆盖。
将某个类设为final,表明你不打算继承该类,即不希望它有子类。
14. interface
接口也可以包含域,但是这些域隐式地是static和final的。
15. 内部类允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。内部类了解外围类,并能与之通信。
内部类能访问其外围对象的所有成员,而不需要任何条件,还拥有其外围类的所有元素的访问权(包括private)。
故内部类的作用有:
- 内部类可以很好的实现隐藏。 一般的非内部类,是不允许有 private 与protected权限的,但内部类可以。
package test; interface Incrementable { void increment(); } class Example { private class InsideClass implements Incrementable { public void increment() { System.out.println("这是一个测试"); } } public Incrementable getIn() { return new InsideClass(); } } public class TestExample { public static void main(String args[]) { Example a = new Example(); Incrementable a1 = a.getIn(); a1.increment(); } }
从这段代码里面我只知道Example的getIn()方法能返回一个Incrementable 实例但我并不知道这个实例是这么实现的。而且由于InsideClass 是private的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。
- 内部类拥有外围类的所有元素的访问权限
public class TestExample { private String name = "test"; private class InTest { public InTest() { System.out.println(name); } } public void test() { new InTest(); } public static void main(String args[]) { TestExample bb = new TestExample(); bb.test(); } }
- 可以实现多重继承
package test; class Example1 { public String name() { return "test"; } } class Example2 { public int age() { return 25; } } public class TestExample { private class test1 extends Example1 { public String name() { return super.name(); } } private class test2 extends Example2 { public int age() { return super.age(); } } public String name() { return new test1().name(); } public int age() { return new test2().age(); } public static void main(String args[]) { TestExample mi = new TestExample(); System.out.println("姓名:" + mi.name()); System.out.println("年龄:" + mi.age()); } }
- 可以避免修改接口而实现同一个类中两种同名方法的调用。
如果你的类要继承一个类,还要实现一个接口,可是你发觉你继承的类和接口里面有两个同名的方法怎么办,你怎么区分它们?这就需要我们的内部类了。
package test; interface Incrementable { void increment(); } class MyIncrement { public void increment() { System.out.println("Other increment()"); } static void f(MyIncrement f) { f.increment(); } } public class TestExample extends MyIncrement { private int i = 0; private void incr() { i++; System.out.println(i); } private class Closure implements Incrementable { public void increment() { incr(); } } Incrementable getCallbackReference() { return new Closure(); } }
16. 容器 P222
- List: ArrayList, LinkedList(添加了可以使其用作栈,队列或双端队列的方法)
- Set (加入Set的元素必须定义equals()方法以确保对象的唯一性)
-
HashSet (出于查询速度的原因,使用了散列。存入hashSet的元素必须定义hashCode())
-
TreeSet (将元素存储在红黑树结构中,方便排序。元素必须实现Comparable接口)
-
LinkedHashSet (也是用了散列,但用链表来维护元素的插入顺序。故使用迭代器遍历Set的时候,结果会按元素插入的顺序显示。元素也必须定义hashCode())
-
- Map: HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap
与Set类似,任何键都必须有一个equals()方法;如果键被用于散列Map,那必须还有hashCode()方法;如果键被用于TreeMap,那它必须实现Comparable。
另外,无论何时,对同一个对象调用hashCode()都应该生成同样的值。且必须基于对象的内容生成散列码。
散列码不必是独一无二的,应该更关注于生成速度。只要通过hashcode() 与equals() 能够完全确认对象的身份。
- Iterator:能够将遍历序列的操作与序列底层的结构分离,统一了对容器的访问方式。
新程序中不要使用已过时的Vector, Hashtable, Stack。
17. 异常
Throwable异常对象可以分为两种类型:Error用来表示编译时和系统错误;Exception是可以被抛出的异常。
运行时异常(RuntimeException)会自动被虚拟机抛出,不用程序员特殊处理。例如NullPointerException
无论异常是否被抛出,finally子句总能被执行。当涉及break,continue时,finally子句也会得到执行。
18. String对象是不可变的。
StringBuffer是线程安全的,开销也会大点。StringBuilder是非线程安全的。
19. 类型信息
一种是传统的RTTI,它假定我们在编译时就已经知道了所有类型;另一种是反射机制,它允许我们在运行时发现和使用类的信息。
对传统的RTTI,编译器在编译时打开和检查.class文件;而对于反射来说,.class文件在编译时是不可获取的,即在运行时打开和检查.class文件。
20. Class对象
每个类都有一个Class对象。每当编写并编译了一个新类,就会产生一个Class对象(确切的说是被保存在一个同名的.class文件中)。为了生成这个类的对象,JVM将使用类加载器。
取得Class对象的一个引用的方式:
- Class c1 = Class.forName("Dog")
- Class c2 = new Dog().getClass()
- 类字面常量:Class c3 = Dog.class
以上三个方法取出来的是同一个对象,即c1 == c2 ==c3 ,但是c1是在运行时加载的,c2、c3是在编译时加载的。
instanceof与isInstance() 保持了类型的概念,即“你是这个类吗,或者是这个类的派生类吗”;但equals()与==,比较的是实际的Class对象,没有考虑继承。
21. 泛型
Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2) // True
在泛型代码内部,无法获得任何有关泛型参数类型的信息,即泛型擦除。上面的例子都会被擦除成原生的ArrayList。
泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof操作和new表达式,因为所有关于参数的类型信息都丢失了。要时刻提醒自己,“它只是个Object” 。
public class TestExample<T>{ private final static int SIZE = 100; public static void f(Object arg){ if(arg instanceof T){} //error T var = new T(); //error T[] array = new T[SIZE]; //error T[] array = (T)new Object[SIZE]; //warning } }
22. 并发
- 线程启动的两种方式:
- 实现Runnable接口并编写run()方法,将该Runnable对象提交给一个Thread构造器,再调用Thread对象的start()方法。
- 直接从Thread继承。
class MyThread implements Runnable{ private int ticket=10; public void run(){ for(int i=0;i<20;i++){ if(this.ticket>0){ System.out.println(Thread.currentThread().getName()+" 卖票:ticket"+this.ticket--); } } } }; public class RunnableTest { public static void main(String[] args) { MyThread mt=new MyThread(); // 启动3个线程t1,t2,t3(它们共用一个Runnable对象),这3个线程一共卖10张票! Thread t1=new Thread(mt); Thread t2=new Thread(mt); Thread t3=new Thread(mt); t1.start(); t2.start(); t3.start(); } }
class MyThread extends Thread{ private int ticket=10; public void run(){ for(int i=0;i<20;i++){ if(this.ticket>0){ System.out.println(this.getName()+" 卖票:ticket"+this.ticket--); } } } }; public class ThreadTest { public static void main(String[] args) { // 启动3个线程t1,t2,t3;每个线程各卖10张票! MyThread t1=new MyThread(); MyThread t2=new MyThread(); MyThread t3=new MyThread(); t1.start(); t2.start(); t3.start(); } }
实现Runnable接口相对于扩展Thread类来说,具有无可比拟的优势。这种方式不仅有利于程序的健壮性,使代码能够被多个线程共享,而且代码和数据资源相对独立,从而特别适合多个具有相同代码的线程去处理同一资源的情况。这样一来,线程、代码和数据资源三者有效分离,很好地体现了面向对象程序设计的思想。还能避免由于Java的单根继承特性带来的局限。因此,几乎所有的多线程程序都是通过实现Runnable接口的方式来完成的。