文章目录
  1. 1. 第二章:一切都是对象
  2. 2. 第三章:操作符
  3. 3. 第四章:控制执行流程
  4. 4. 第五章:初始化与清理
  5. 5. 第六章:访问权限控制
  6. 6. 第七章:复用类
  7. 7. 第八章:多态
  8. 8. 第九章、接口
  9. 9. 第十章、内部类

第二章:一切都是对象

  1. 用引用操作对象

每种编程语言都有操纵内存数据的方式,那么程序员需要考虑怎样使用这些内存中的数据。比如是直接操纵数据单位,还是通过特殊语法间接使用这个数据单位。在C/C++中,程序员通过指针来间接使用数据单位。但是在Java中这一切得到了简化。一切都被视为对象,因此可采用单一固定的语法。操纵的标识符实际上是一个对象的引用。

  1. 数据单位存在哪里?

在Java主要有5类:

寄存器:CPU中,最快的存储区。但是空间太少(程序员无法操控)
堆栈:通用RAM(随机访问存储器),对象的引用存储在这里,通过堆栈指针来开辟/释放内存。[特殊情况是8种基本类型]
堆:通用内存池(也位于RAM区),存放所有Java对象
常量存储:存放在程序代码内部,或者可以选择ROM(只读存储器)。这一般是编译器的职责,在编译过程中可能会将使用该常量的地方进行替换
非RAM存储:数据存活在程序之外,它可以在程序运行中存在,也可以在程序终止时存在,不受程序的限制。
流对象:字节流在机器之间传输
持久化对象:持久化对象保存在磁盘上
但是也有例外,就是基本类型。在Java中,new出来的对象会存放在堆中。但是对于特别小的、简单的变量,往往效率很低。所以Java规定:

对于8种基本类型,是通过创建一个并非是引用的“自动”变量。这个变量的值直接存储“值”,放在堆栈中。

  1. 移植性的原因:一致性

Java会固定每种基本类型所占存储空间的大小。它们的大小并不像大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是Java程序比大多数其它语言编写的程序更具有移植性的原因之一。比如C/C++中,int的大小可能会是2字节或者是4字节,这就给平台移植带来了一定的麻烦。下面是Java的基本数据类型:

基本类型 大小(字节) 包装器类型
boolean - Boolean
char 2 Character
byte 1 Byte
short 2 Short
int 4 Integer
long 8 Long
float 4 Float
double 8 Double
下面是一些tips:

在C/C++中,有unsigned类型,但是Java中没有。所有数值类型都有正负号,所以不要去寻找无符号的数值类型
boolean仅仅定义了true和false,所以boolean类型所占存储空间的大小不确定。

  1. Java数组安全

对于数组而言一般有两大问题:

未初始化就使用
数组越界
在C/C++中,使用未初始化的数组元素或者数组越界是不会报错的,所以程序会出现莫名其妙的错误,不容易定位错误的地方。而Java的主要目标之一就是安全性,所以Java对于这两点有相应的处理方法:

Java确保数组会被初始化后才能使用。如果是基本类型,直接用默认值初始化;如果是引用类型,就用null来初始化。如果不把引用指向对象,Java一看引用是null,就会抛出NullOfPointerException。
Java对数组采用了少量的内存开销和运行时下标检查作为代价,我估摸就是对每个数组记录一下长度length,如果你使用的下标为index,Java会判断index是否小于length,如果小于,正常使用;反之抛出ArrayOutOfBoundsException。

  1. GC机制

C++饱受诟病的一点就是内存泄露,因为C++对于效率的追求较高,所以将内存处理也交付程序眼自己处理。而对于内存操作不当,很容易就造成内存泄露问题。但Java解决了这个问题(当然,带来的影响就是Java程序性能会下降)。它采用一个垃圾回收器,用来监视用new创建的所有对象,并辨别那些不会再被引用的对象(就是引用个数为0),之后会释放它们占有的内存供其它新的对象使用。

  1. 注释和嵌入式文档

对于很多公司来说,开发工作都是很紧的,这带来的问题就是程序没有详细的设计文档,别人无法从整体上把握程序。而Java运用Javadoc工具,将程序和文档“链接”起来:程序员写特殊的注释语法,然后Java提取整理,输出一个html页面,可以用浏览器查看。

所有的Javadoc命令都只能在/*注释中出现,和通常一样,注释结束于/。使用Javadoc的方式主要有两种:

嵌入html(很少用…)
使用独立文档标签(是一些以@字符开头的命令,且要置于注释行的最前面)
对应于类、域、方法,注释方法如下所示:

/* A class comment /
public class HelloWorld {

/** A field comment */
public int number;

/** A method comment */
public void function() {
}

}
但是对于private和包内可访问成员,Javadoc会忽略。因为只有public和protected的成员才能在文件之外被使用,这是客户端程序员所期望的。

一些常用的@命令有:

@see:引用其他类
@version:版本说明信息
@author:作者
@param:方法的参数列表标识符描述
@return:函数返回值描述
@throws:抛出异常描述
@Deprecated:废弃的方法
举一个例子吧:

import Java.util.*;

/**

  • @author niushuai
  • @version 1.0
    */

public class HelloWorld {
/** Entry point to class & application

* @param args array of string arguments
* @throws exceptions No exceptions thrown
*/

public static void mian(String[] args) {
    System.out.println("Hello world!");
}

}

  1. 编码风格

在“Java编程语言编码约定”中,代码风格是这样规定的:

类名中所有单词首字母大写
几乎其他所有内容——方法、字段(成员变量)以及对象引用名称等,普遍采用首字母小写,后面单词首字母大写
static final修饰的变量全部大写,单词之间用下划线分隔
括号在参数列表行内
最大行宽80字符
缩进(tab)2个字符

第三章:操作符

Java中的数据都是通过使用操作符来操作的。

  1. Java引用的一个坑

Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
如果是一个C/C++程序员,毫不犹豫就会得出答案为:true false。但是在Java中,”==”比较的只是n1和n2这两个引用,而它们指向了不同的对象,所以它们是不相等的。正确答案应该是false true。那么,如果我想比较n1和n2指向的对象是否相等呢?答案是使用equals(),比如n1.equals(n2)即可。而这个equals()也不简单,下面来谈谈。

  1. 相等问题之——equals()和hashCode()

在Java使用中,经常会碰到需要判断引用/对象是否相等的情况。但因为Java本身的语言特性,这点特别容易产生bug,所以应该彻底搞清楚才行。

首先判断相等有两种:

引用相等
对象相等
举个例子:

class A {
String name;
}

public class B {
public static void main(String[] args) {
A a = new A();
A b = new A();
System.out.println(a == b); //引用相等
System.out.println(a.equals(b)); //对象相等
}
}
引用相等:a和b都是引用,但因为new了2个对象,a和b指向的不是同一个对象,所以这里的结果是false。
对象相等:因为A是自定义类型,而且没有重载equals(),将使用Object类的equasl(),实际上调用的还是”==”,也就是判断引用相等。所以结果也是false。
如果想要获得对象相等,先得知道Object类定义的hashCode()和equals():

hashCode()的默认行为是对堆上的对象产生一个hash值(一般是根据内存地址计算得到的)。如果你没有重载过hashCode(),不同对象拥有不同的内存,两个对象肯定不可能相等。
equals()的默认行为是执行”==”的比较。也就是上面说的,比较的是是否指向的都是堆上同一个对象。如果没有重载equals(),默认行为中的两个对象的两个引用肯定不会相同,所以equals()肯定是false。

如果两个对象使用equals()相等,那么hashCode()也必须相等
如果两个对象的hashCode()相等,这两个对象不一定相等(因为hash会产生碰撞)

  1. Java不必小心把”==”写成”=”了

在Java中,“与”、“或”、“非”操作只可以应用在布尔值上面。与C/C++不同的是,不可将一个非布尔值当做布尔值在逻辑表达式中使用。比如:

int i = 5;
while(i) {
//do something
}
这样是错误的。因为i是一个整型,而不是一个布尔类型。(在C/C++可以这样使用,因为C/C++会进行隐式类型转换)

另外一点,在C/C++中有时如果一不小心,我们可能写出这样的代码:

while(x = y){
//do something
}
由于进行的是赋值操作,而且C/C++会进行隐式类型转换,所以循环会执行。但是对于Java而言,不会把非布尔类型转化为布尔类型,在编译的时候就会报错,所以不会出现这样的问题。

因此,在Java中,一般不会出现这样的错误(除非x和y都是布尔类型的)。如果知道这点的话,在Java程序中就不用反人类的写出while( “hello” == string)这样的代码了。

第四章:控制执行流程

  1. 逗号表达式

在Java中唯一用到逗号操作符的地方就是for循环的控制表达式了,注意:在参数列表中使用的逗号是逗号分隔符。for循环可以这样:

for(int i = 1, j = i + 10; i < 5; ++i)

  1. goto有害论和标签

Dijkstra曾经专门说过goto有害。其实,编程语言早就有goto了,它源于汇编语言:若条件A成立,则跳到这里;否则跳到那里。虽然goto仍然是Java的一个保留字,但是在语言中并没有使用它,相当于Java没有goto。额,Java竟然“别出心裁”的创造出了标签这玩意儿:

标签是后面跟有冒号的标识符,比如label1:
在Java中,标签起作用的唯一地方刚好是在迭代语句前面。“刚好之前”的意思表明,在标签和迭代之间置入任何语句都不行。而在迭代之前设置标签的唯一理由是:我们希望在其中嵌套另一个迭代或者一个开关。因为break和continue通常只是中断当前循环,但若随同标签一起使用,它们就会中断循环,直接到达标签所在的地方(和goto的作用有区别吗??????真心理解不动啊= =!)

public class LabelFor {
public static void main(String[] args) {
int i = 0;
outer: for (; true;) {
inner: for (; i < 10; i++) {
System.out.println(“i = “ + i);
if (i == 2) {
System.out.println(“continue”);
continue;
}
if (i == 3) {
System.out.println(“break”);
i++;
break;
}
if (i == 7) {
System.out.println(“continue outer”);
i++;
continue outer;
}
if (i == 8) {
System.out.println(“break outer”);
break outer;
}
for (int k = 0; k < 5; ++k) {
if (k == 3) {
System.out.println(“continue inner”);
continue inner;
}
}
}
}
System.out.println(“finish”);
}
}
/* 输出:
i = 0
continue inner
i = 1
continue inner
i = 2
continue
i = 3
break
i = 4
continue inner
i = 5
continue inner
i = 6
continue inner
i = 7
continue outer
i = 8
break outer
finish
/
规则总结了一下就是这样:

一般的continue会退回最内层循环的开头,并继续执行
带标签的continue会到达标签的位置,并重新进入紧接在那个标签后面的循环
一般的break会中断并跳出标签所指的循环
带标签的break会中断并跳出标签所指的循环,跳过这个标签后的整个循环
法则就是:

使用标签的唯一理由就是因为有循环嵌套存在,而且像从多层循环中break或者continue(就是跳过>=2层循环使用break和continue)

第五章:初始化与清理

本章主题:

程序很多情况下,不安全原因无非两个:初始化和清理。C++提出了构造函数和析构函数。为了提高效率,这两项工作均由程序员完成;而Java仅提供了构造函数供程序员使用,对于清理工作,Java自带垃圾回收器。虽然安全性有了保证,但是也牺牲了一定的效率。

  1. this关键字

假如A类有一个fun(int i)函数,现在A类创建了2个对象,2个引用指向了这两个新对象,2个引用为a和b。那么a和b调用fun(int i)的时候,fun(int i)怎么区分哪个是a哪个是b呢?其实这个工作是通过this实现的。编译器在编译的时候,会把a.fun(1)和b.fun(2)转化成A.fun(a, 1)和A.fun(b, 2)的。当然,这是编译器在幕后做的,我们使用的时候不能写成这种形式。

  1. 构造函数中调用构造函数

在构造函数中调用其他构造函数时,有两个规定:

this调用其他构造函数必须是当前构造函数的第一个语句。然后就很清楚了,前面不能有任何其他语句(所以不能有2个及以上个this调用。因为this调用必须是第一个语句,所以第二个this调用前面也有一个语句(不管是什么语句,只要有,就出错)。
只能在构造函数中才能通过this调用构造函数,在非构造函数中不能通过this调用构造函数。

  1. 对static的一点讨论

对于static来说,其实有一些争议。因为我们知道static是类的属性,与对象无关。但是Java标榜自己是完全面向对象的程序语言。类与类之间的沟通完全是通过对象的消息进行通信的。但是static却和对象没有关系,所以违背了Java的面向对象的说法。但是,我们知道事情都有两面性,在适当的时候能运用static还是非常有必要的,只是如果你程序中大量出现static的时候,就应该重新考虑一下设计的是否合理了。

  1. Java垃圾回收

首先大概了解一下Java垃圾回收机制的几种工作方式:Java垃圾回收机制

时刻牢记:使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。意思就是说,如果你的程序中有一个地方跟内存无关,那么垃圾回收器就不会管。比如Java中调用的C/C++程序,而C/C++程序申请/释放内存的话与Java无关,那么Java垃圾回收器就不会对这个内存起作用。而finalize()就是为解决这个问题提出的,先看以下三点:

对象可能不被垃圾回收
垃圾回收并不等于“析构“
垃圾回收只与内存有关
Java引入finalize()的原因是因为在分配内存时可能采用了类型C语言中的语法,而非Java中的通常做法。这种情况多发生在使用”本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C/C++,但是在C/C++中又可以调用其他语言,所以从本质上来说,Java程序中可能出现所有编程语言。当然,每种编程语言有自己不同的内存分配策略,因为这与Java无关,所以Java的垃圾回收机制就不会作用于这些内存,当然需要程序员自己处理了。比如C代码中通过malloc()分配存储空间,除非调用了free()来释放这些存储空间,否则存储空间是不会释放的,这样就会造成内存泄露。当然,free()是C中的函数,必须在finalize()中用本地方法(这里是C方法)来调用它。

  1. 初始化

package Chapter05;
class InitTest {
public static int number = 2014;
public int i;
public int j = 3;
InitTest(int i) {
this.i = i;
j = 10;
}
public void print() {
System.out.println(number);
System.out.println(i);
System.out.println(j);
}
}
public class InitOrder {
public static void main(String[] args) {
InitTest initTest = new InitTest(20);
initTest.print();
}
}
当首次创建类型为InitTest的对象时(构造函数其实是static方法),或者InitTest类的静态方法/静态属性首次被访问的时候,Java解释器必须查找类路径,定位InitTest.class文件
然后载入InitTest.class(将创建一个class对象),有关静态初始化的所有动作都会执行,因此静态初始化只在class对象首次加载的时候进行一次。
当用new InitTest()创建对象,在堆上为InitTest对象分配足够的存储空间
这块存储空间会被清零,自动将InitTest对象中的所有基本类型数据设置成默认值(对数字来说就是0,对布尔类型和字符型也相同),而引用则被设置为null
执行所有出现于数据定义处的初始化动作(就是7中的第三条)
执行构造函数。
所以上面的例子工作流程是这样的:

当在InitOrder中调用了public static void main(String[] args)就触发了InitTest的静态main方法,然后Java解释器通过CLASSPATH查找类路径,定位InitTest.class
载入InitTest.class,将number初始化为2014
在堆上为InitTest分配存储空间
数据域清空,i和j都会被置0(引用会被置null)
j在定义初始化了,所以j被重置为3
执行构造函数,i被重置为20,j被重置为10
有两点需要注意的是:

对于static域,可以在定义的地方初始化,也可以在构造函数初始化。所以不能认为构造函数只是初始化非static域。而且对于static域,可以将static变量统一赋值。类似这样:
static int i, j;
static {
i = 1;
j = 2;
}
对于普通变量也可以使用类似的方法,只要在”{“前面去掉static就可以了。这种语法对于支持“匿名内部类”的初始化是必须的,但是它也使得你可以保证无论调用了哪个显式构造函数,某些操作都会发生。

对于局部变量,你不初始化,编译时候肯定出错。因为局部变量说明程序员在这里用到才设置的,所以要提醒你。比如在C++中定义这样的
{
int i;
i++;
cout<<i<<endl;
}
你运行的时候都不会出错,但是这个i到底是多少就不知道了。所以排查起来非常晦涩;而在Java中,你编译的时候就会出错(对于Eclipse来说,你写完这句话就会出现错误提示,提示你本地变量没有初始化),所以Java这点:编译器可以为它赋值,但是未初始化的局部变量更可能是程序员的疏忽,采用默认值反而会掩盖错误。因此强制程序员提供一个初始值,往往能够找出程序的缺陷。

  1. 创建数组的一个坑

如果是基本类型,你创建之后就可以使用了。默认初始化为0
如果是对象,那么你创建的只是一个引用数组,数组中的元素都是引用,但是没有指向具体的对象。所以你使用之前必须让它们指向对象

第六章:访问权限控制

  1. 知识点
    CLASSPATH变量就是让编译器找到.class文件和jar包用的。对于.class文件,你指定目录就可以了(一般为bin目录);而对于jar包,因为是独一无二的,所以我们必须指定完整的路径,比如/data0/lib/rt.jar。
    指定CLASSPATH之后,当编译器遇到库的import之后,就开始在CLASSPATH所指定的目录中查找,然后从.class文件中找出对应名称的类。之后就可以正常使用了。
    Java的默认包也具有包访问权限。也就是说,如果你在一个项目中,没有为某N个编译单元指定包结构,那么它们就处于默认包下面,这样等同于具有共同的包结构,所以它们可以互相调用。
    首先Java没有C那样方便的条件编译功能,以前在C中,我们可以在代码中使用条件编译,最方便之处就是可以一个标记完成运行/调试,-DFLAG即可。而Java去掉此功能的原因是这样的:
    C在绝大多数情况下是使用此功能来解决跨平台问题的,即程序代码的不同部分是根据不同的平台来编译的。由于Java自身可以自动跨越不同的平台,因此这个功能对Java而言是没有必要的。
    看head first Java竟然没注意import static这个用法,说白了,静态import就是少打几个字,但是会让程序的可读性降低。

  2. Java权限访问

主要有4个权限修饰,范围从大到小排序后:
public:import之后包内包外可以使用
protected:继承访问权限,只有子类可以访问。同时,protected也提供包访问权限
包访问权限:(类前面没有权限修饰符)一个包的代码可以访问本包内所有代码,但是本包对外界成员是透明的、不可见的
private:除本类外包内包外均无法访问
控制对成员的访问权限有两个原因:

为了使用户不要碰触那些他们不该碰触的地方,这些部分对于类内部的操作是有必要的,但是它并不属于客户端程序员所需接口的一部分
为了重构。只要当前版本提供的公共接口没有改变,就不会因为底层数据/方法(设为private)的改变受到影响。然后我们开发完成新的功能后只需要添加新的接口,就不会影响原有代码,在以后版本使用中使用新的接口就可以了。
当然,上面的访问权限主要是针对类的数据成员的,而对于类来说,一般只有两种:public和包访问权限。因为private将使得除本类以外的其它类都无法访问该类;而protected则是该类的子类和包内能够访问,没有对外接口,也不合适。

第七章:复用类

既然是讨论代码的复用,那么肯定涉及到了组合和继承的概念,另外代理也是一种常用的代码复用手段。

has a:组合
is a:继承
中庸之道:代理
然后就是如何使用这三个法则选择对应的解决方案。对于更复杂的要求(比如实现C++的多继承问题),接口就会出来打酱油了- -

  1. 组合、继承、代理

理清这三者之间的关系是我们做出设计的第一步。在《Java编程思想》中,作者建议慎重使用继承,当我们确定要不要使用继承时,有一个很清晰的方法:

需要向上转型吗?
在上面我们说过,组合是解决has a问题,继承是解决is a问题,而这里之所以要说向上转型,其实是为后面的多态做铺垫。这样,我们就可以完成面向接口编程的壮举。你只需要定义一个基类,就可以通过调用相同的方法传递不同的消息给不同的对象,极大的简化了程序的复杂度,完成了代码的复用。就像马士兵的“Sping教程”里的例子,运用MVC模型,一个数据库接口,可以操纵mysql,可以操纵oricle,可以操作xx数据库。

tips:

为了继承,一般的规则是将所有的数据成员都设置为private,而将所有的方法都设置为public。这样,当不同的包下的类继承该类时,就可以获得该类所有的方法,和包内、包外没有区别。如果不加修饰符,就是限制包内访问,那么包外继承的时候,只能获得public修饰的方法,这样内外的方法不一致,就会出现问题了。当然,特殊情况需要特殊考虑。

  1. final用法

根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。不能做改变可能处于两种理由:设计和效率。由于这两个原因想差很远,所以可能会被误用。这里简单总结一下它们应用的场景。具体的,Java中的final有三种使用场景,分别为数据、方法和类:
final数据:
一个永不改变的编译时常量: 因为编译期常量可以带入用到它所在的表达式,就可以免去运行时的工作。由于编译的时候是无法初始化自定义类的,所以必须为8种基本类型,而且在定义的时候初始化。对于引用来说,用final修饰就表示这个引用(数组也是一种引用)只能指向这个对象,不能指向别的对象(但对象本身可以改变,只是引用不可改变)
一个运行时被初始化的变量,而你不希望它被改变,比如空白final的使用:可以通过在定义处或者在构造函数中初始化,而在构造函数中初始化可以使不同对象的对象拥有不同的值(比如身份证号,唯一且不能改变)。这样就保证final数据在使用前被初始化且无法改变的特征。
final方法使用的原因有2:
把方法锁定,防止任何继承的子类修改它的含义(比如查看某一重要数据的方法,任何方法无法改变)
效率:早期Java会将final方法转为内嵌调用。类似C++的内敛函数(inline),但随着Java性能的提升,这个做法基本废除了
final类:表明了你不打算继承该类,而且也不允许别人这样做。你对该类的设计并不需要改变,或者是处于安全的考虑,你不希望它有子类。
tips:

类中所有的private方法都隐式指定为final的,因为无法取用private方法,所以无法覆盖它。就好像构造函数是隐式指定为static类似。
在Java中,如果基类有一个方法是private的,那么,在子类中可以使用完全相同的函数(包括所有东西)。因为基类的private方法子类是看不到的,所以你定义一个完全相同的函数子类也会认为是新函数,当然不会有问题。但是如果你使用@Override注解,就明确告诉编译器这个方法是覆盖基类的方法,但因为子类“看不到”基类的这个private方法,@Override注解就会报错,说你必须覆盖一个基类存在的方法。

  1. 重载、覆盖、隐藏
    Java没有所谓的名称屏蔽(隐藏),就是如果Java的基类拥有某个已被多次重载的方法名称,那么在子类中重新定义该方法名称并不会隐藏在基类中的任何版本。

  2. 初始化
    在第五章讲到了初始化,但是因为没有涉及到继承,所以还不算完整。这里就详细说一下在有继承的情况下是如何完成初始化的。

在执行xx.main的时候,因为main是static方法,就触发了该类的初始化。于是加载器开始启动并在CLASSPATH中找到xx.class,如果xx继承自aa,那么编译器注意到xx有一个基类aa(通过extends得到),就开始加载基类,不管你是否打算产生一个该基类的对象。如果该基类继承自其它类,就会以此类推。
根基类中的static初始化会被执行,然后是第二个基类……
在堆上生成对象
基本数据置0,引用置null
定义处的域初始化
从根基类的构造器开始执行,一直到底

第八章:多态

  1. 向上转型

在前面简单说过向上转型的问题,从根本上来说,动态绑定是多态的核心。而实现动态绑定就需要借助向上转型的力量。定义一个基类的接口,然后由出现类继承这个接口以后实现各自的功能。这样,当我们定义一个基类对象时,我们可以根据需要用继承自该类的任何子类来初始化它(也就是向上转型),因为基类定义了相同的接口,而实现是在子类中,子类可以有不同的实现,那么我们通过这一个基类的接口就可以实现不同的功能,从而实现多态。

  1. 动态绑定

从上面我们知道,向上转型是多态的核心。而理解向上转型我们就必须弄懂Java的动态绑定。那么,什么是静态绑定,什么是动态绑定呢?

静态绑定:在执行前(由编译器和连接程序实现)就可以明确确定调用哪个函数,典型的例子就是C函数,因为C程序不允许重载,所以你使用fun(30),编译器就确定你使用的是fun()是哪个。又比如static和final,因为都是不变的,所以就是静态绑定
动态绑定:顾名思义,和静态绑定不同,动态绑定必须在运行时才能确定是哪一个方法被调用。比如fun(30)因为可以定义多个fun(),比如fun(int), fun(Long), fun(char)……,所以只有在运行的时候才能确定要调用哪一个具体的函数。在Java中,除了static方法和final方法(private方法属于final方法)之外,其它所有的方法都是动态绑定
举一个非常简单的例子来说明静态绑定和动态绑定:

//静态绑定
int fun(int n) {
return n * 2;
}
int main(void) {
int i = 3;
fun(i);
return 0;
}
//动态绑定
package Chapter08;
import Java.util.Random;
class A {}
class B extends A {}
class C extends A {}
class D extends A {}
class RandomNow {
private Random random = new Random(100000);
public A shuffle() {
switch(random.nextInt(10)) {
case 0:
case 1:
case 2:
case 3:
return new B();
case 4:
case 5:
case 6:
return new C();
case 7:
case 8:
case 9:
return new D();
default:
return null;
}
}
}
public class RandomMe {
public static void main(String[] args) {
RandomNow randomNow = new RandomNow();
A[] a = new A[10];
for(int i = 0; i < a.length; ++i) {
a[i] = randomNow.shuffle();
}
for(int i = 0; i < a.length; ++i) {
System.out.println(i + “ “ + a[i].getClass());
}
}
}
上面例子很明显,在静态绑定中,编译器能准确知道fun()就是唯一的fun(int),而在动态绑定中,编译器根本无法知道RandomNow.shuffle()返回的A具体是B、C、D中的哪一个。而这恰恰就是动态绑定完成的任务:在运行时确定。

  1. 多态的缺陷一:“覆盖”私有方法

public class PrivateOverride {
private void fun() {
System.out.println(“Private fun()”);
}
public static void main(String[] args) {
PrivateOverride privateOverride = new Derived();
privateOverride.fun();
}
}
class Derived extends PrivateOverride {
public void fun() {
System.out.println(“Public fun()”);
}
}
/* output:
Private fun()
/
我们想输出的是Public fun(),但因为private的fun对于Derived是不可见的,所以Derived中的fun是一个全新的方法,不是覆盖。所以对于子类没有覆盖基类的情况,肯定是调用基类的fun

  1. 多态的缺陷二:域与静态方法

package Chapter08;
class Super2 {
//1. 一般情况下,父类的域都设置为private
//2. 不会对基类中的域和子类中的域起相同的名字,容易混淆
public int field = 0;
public int getField() {
return field;
}
}
class Sub2 extends Super2 {
public int field = 1;
public int getField() {
return field;
}
public int getSuperField() {
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super2 super2 = new Sub2();
System.out.println(“super2.field = “ + super2.field

            + ", super2.getField() = " + super2.getField());
    Sub2 sub = new Sub2();
    System.out.println("sub.field = " + sub.field + ", sub.getField() = "
            + sub.getField() + ", sub.getSuperField() = "
            + sub.getSuperField());
}

}
/* output:
super2.field = 0, super2.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
/
我们看到,为Super.field和Sub.field分配了不同的存储空间。这样,Sub相当于拥有了2个field域。但是在引用Sub中的field所产生的默认域并非Super版本的field域。因此,如果想要得到Super.field,必须显式指定Super.field。

结论就是:

Java中,只有普通的方法调用是多态的。然后在对待域的问题上,这个访问将在编译期进行解析,因此不是多态的。同理,静态方法是与类绑定的,跟对象无关。所以,静态方法也不具有多态性。
解决方法:

既然多态的缺陷只要在于private方法、域、静态方法,那么我们就记住对于private方法子类是完全看不见的;对于域来说,基类要设置为private,修改/访问通过get/set方法,而且避免基类、子类使用相同的域名称;对于静态方法,因为是类属性,所以不会因为不同对象消息的改变而改变。
在Java中,除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判断是否应该进行后期绑定——它会自动发生。
那么,我们现在就可以探讨一下使用final修饰一个方法的原因了。就是防止别人覆盖该方法。但更重3要的一点是:这样做可以有效的关闭动态绑定,或者告诉编译器这个方法不需要进行动态绑定。这样,编译器就可以为final方法生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观,所以使用final的目的是考虑是否能被覆盖,而不是效率,谨记。

  1. 初始化总结

这个在第五章、第七章已经出现了,但是有了多态之后,又特么的出现了。于是,怒总结之。

调用xx.main的时候,因为main是static,编译器通过CLASSPATH找到xx.class文件
加载xx.class的时候,发现它是extends yy,那么就同样通过CLASSPATH找到yy.class,依此类推,找到根基类
对静态域/静态方法初始化
在堆上分配内存,并直接通过二进制清零,引用置null
对定义时初始化的非静态域进行再初始化
数据域全部搞定之后,执行构造函数体(这时候执行构造函数的原因是:构造函数具有一项特殊的任务:检查对象是否被正确构造。所以,必须对象已然存在在堆上才能检查,再进行下一步的初始化操作)

  1. 继承与清理

既然构造都说了半天,清理工作也是必须明白的。

其实如果有特殊情况需要清理,那么一定要注意动态绑定*的副作用,它调用的是导出类的清理函数,如果基类也需要清理,就必须显式的使用super.清理函数来完成。同时,销毁的顺序应该和构造的顺序相反。

  1. 一个多态引起的隐患

我们知道,用导出类初始化基类,调用一个导出类覆盖过基类的函数是正常的。但如果发生在构造函数中,就会出现问题。下面有个例子:

package Chapter08;
class Glyph {
void draw() {
System.out.println(“Glyph draw()”);
}
Glyph() {
System.out.println(“Glyph before draw()”);
draw();
System.out.println(“Glyph after draw()”);
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int i) {
radius = i;
System.out.println(“RoundGlyph.RoundGlyph, radius = “ + radius);
}
void draw() {
System.out.println(“RoundGlyph.draw(), radius = “ + radius);
}
}
public class PolymorphismConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
/* output:
Glyph before draw()
RoundGlyph.draw(), radius = 0
Glyph after draw()
RoundGlyph.RoundGlyph, radius = 5
/
在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零
调用基类的构造器,但因为基类都没有构造成功,导出类更不可能构造成功了,所以radius的值为0
教训就是:

用尽可能简单的方式使对象进入正常状态;如果可以的话,尽量避免调用其他方法。在构造器内唯一能够安全调用的就是基类中的final方法了(private同属于final),这些方法不能被覆盖,所以就不会出现上面的问题了。

第九章、接口

  1. 完全解耦

这个算是设计模式方面的东西吧,还不算理解。但是稍微知道一点就是在代码设计中,尽量避免高耦合的情况出现。如前面项目中出现的问题就是由于高耦合引起的:

业务A需要业务B处理,而且是完成之后才能继续干活。那么,在压力比较大的情况下,B处理不过来的时候,就会挂掉,或者处理时间过长。那么就会引起A无缘无故等待过长时间,这样A等待的时间长,干活的时间少。时间长也会挂掉。这样,由于高耦合性,B挂掉也会连累A。后来,通过一个缓冲层将A/B解耦合,那么,无论A/B谁挂掉,都不会影响对方的工作。

  1. 多重继承

这个算是这章的重点,也是Java的重点。在C++中,一个类通过继承多个基类完成多重继承,而Java由于只允许单继承,所以要实现多重继承就要借助其它方法——接口+类(类或者抽象类)+内部类,一般情况下是单继承一个类,然后通过实现接口(本类或者内部类)来完成多重继承的任务。

这里比较困难的就是:extends只有一个,我该extends谁?剩下的implements到底是本类执行还是内部类执行?总的来说就是2个选择:

extends谁
implements interface由本类做还是内部类做?
这其实是一个好设计的基础,按我的理解,extends因为只有一个,所以要专注最重要的特性,同时,在将复用的时候说过,判断继承的原则就是是否需要向上转型。我们认真思考一下就知道了,这个子类一定算是父类公开接口的某一个实现(一般情况下是多个子类实现父类的接口,然后完成动态绑定的任务)。那么,extends的就是最重要的特性。

而对于implements,能用本类的情况就不要用内部类,因为内部类一般情况是想实现“内部”的功能,想对使用者透明(就比如没个容器自己实现一个private的迭代器)。另外一点,内部类的本质是为了使多重继承更加完整。所以下一章会努力找到“什么时候使用内部类”的答案。

第十章、内部类

这一章讲述的是内部类,以前觉得这东西随便一节介绍介绍就好,但我估计是没认识到内部类的好处。因为《Head First Java》和《Thinking In Java》都把内部类作为单独的一章来讲。所以看完这一章再回来总结一下:

为什么要存在内部类
什么情况下该使用内部类
填坑ing…在10.8节中道出了内部类的本质作用【解答上章最后的问题】:

完善Java多重继承。内部类通过继承多个非接口(类或者抽象类),使多重继承的方案更加完整可行。因为是单继承,不可能有多个extends。而非接口只能通过extends实现,所以自然而然就是内部类的活了。
我们知道,C++中有多重继承的概念,但随之而来的就是菱形继承的噩梦。Java设计者避免了这个做法,但多重继承也是一个使用频繁的功能,于是Java运用单继承+接口+内部类的方向完成了相同的功能。虽然稍微麻烦点,但是也一定程度避免了C++的问题。算是有得也有失吧。

然后这一章看的不细致,因为里面很大一部分是有点偏的知识,我打算先把精华看一下,等我下遍复习的时候从整体上来把握细节。额,现在看的确实有点晕乎了- -!~

  1. 什么是内部类

顾名思义,内部类就是在类内部定义的类,但是它有一个天然的属性特别重要:

当生成一个内部类的对象时,此对象与生成它的外部对象之间就有了联系,所以它能访问其外部类的所有成员,而不需要任何特殊条件。
我们一想,这敢情好啊。内部类和外部类之间完全没有隔阂,数据通信无障碍(private都不是个事)。但它是怎么实现的呢?

当某个外部类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类的引用。然后,在你访问外部类的成员时,就是用那个引用来访问外部类的成员。而且,这都是编译器帮我们搞定的。

  1. 匿名内部类

多线程时候用到过,Runnable相当于一个任务,Thread相当于一个工人。那么我们把这个任务给工人后,告诉它start()就可以执行了。这时候,交付任务就是通过匿名内部类实现的,举个例子:

public void testNoNameInnerClass() {
executor.execute(
new Runnable() {
public void run() {
//do something
}
}
);
}

  1. 工厂设计模式

写了一篇文章,还是写点代码能清楚点。工厂模式

  1. 嵌套类

如果不想让内部类和外部类有联系,我们可以将内部类定义为static,而这就是嵌套类。其实也很容易理解,不带static的话,内部类对象会隐式的保存一个外部类的引用,指向创建它的外部类;而声明为static之后,因为它是外部类的属性,就没有隐式的引用。所以你创建内部类的时候就不需要外部类的羁绊了。

文章目录
  1. 1. 第二章:一切都是对象
  2. 2. 第三章:操作符
  3. 3. 第四章:控制执行流程
  4. 4. 第五章:初始化与清理
  5. 5. 第六章:访问权限控制
  6. 6. 第七章:复用类
  7. 7. 第八章:多态
  8. 8. 第九章、接口
  9. 9. 第十章、内部类