Thinking in Java 笔记

  大二就买了这本书,如今再看这本书,看到了一些以前没看的细节,也有了不同的体会。本文使用第4版,整理每章的笔记心得。老外的书有个特点,他会花费大量的文字去阐述一个概念,这比堆代码强多了。

第 1 章 对象导论

1.1 抽象

  抽象是计算机最重要的概念之一。抽象就是从杂乱的事物表象中,提取出对待解决问题来说最关键的部分内容。C 在解决问题时,主要是基于计算机的结构进行抽象,而不是基于所要解决的问题的结构。而 Java 则是针对问题进行建模,根据问题来描述问题,程序可以通过添加一个新类型的对象使自身适用于某个特定问题,程序员不会受限于任何特定类型的问题。

  对象具有状态、行为和标识。纯粹的面向对象程序设计的基本特性:

  • 万物皆为对象:对象可以存储数据(状态),也能执行某些操作(行为)。理论上,可以抽取待解决问题中的任何概念化构建为对象。
  • 程序是对象的集合,它们通过发送消息告知彼此所要做的:其实就是调用特定对象的方法。
  • 每个对象都有自己和其他对象构成的存储:通过现有对象生成新对象,继承和组合。
  • 每个对象都拥有其类型:每个对象都是某个类的实例 。
  • 一类对象可以接收同样的消息:多态,子类能够替代父类完成某些调用。

1.2 类和对象

  每个对象都是某个类的实例,一个类实际上就是一个抽象数据类型,它描述了具有相同特性(成员)和行为(功能)的对象集合。程序员可以通过定义类来适应问题,而不是被迫使用现有的用来表示机器中的存储单元的数据类型。根据需求添加新类型扩展编程语言,系统就像对待内置类型一样验证并管理它们。

  把对象看做是服务提供者好处:将问题分解为一系列对象的集合;提高对象的内聚性,每个对象都专注本职工作,就是高内聚性,还有个低耦合,解耦一般就用队列实现;方便理解代码和重用。

  面向对象的特点:封装、继承和多态。何为封装?即隐藏对象的属性和细节,仅对外公开接口,控制属性的读取和修改。访问控制存在的原因:隔离与提供的服务无关的部分;分离并保护接口和实现。Java 中有四种访问权限,分别是:

  • public:对任何人都可访问 
  • private:只有类创建者和内部方法可访问
  • protected:与 private 相当,区别是子类可访问
  • 默认访问权限:同一包内的类可相互访问,不同包如同 private 一样

1.3 代码复用

  两种方式,组合和继承,组合灵活性比较高,相比于继承耦合度低一些。如果要使用某个类提供的服务功能时,一般用组合,当要是使用类提供的接口时使用继承。

  继承,使用现有的类创建新类型。子类拥有父类的成员(public, protected)并且复制了父类的接口,也就是说,子类与父类具有相同的类型。子类有两种方式改变自己的行为:添加新方法和覆盖父类的方法。当添加新方法时,如果所有子类都需要,就可以把它抽取到父类中。

  Java 中,Object是所有类的直接或间接父类。单根继承的好处有:所有对象具有共用接口,利于向后兼容;所有对象都具备某些功能,易于创建和参数传递;易于垃圾回收。

1.6 多态

  既然父类和子类是相同类型,那么在运行时子类就能替换父类(向上转型)来完成不同的功能,这就是多态。多态的体现是:方法的重载和覆盖。编译器(静态分发,重载)和运行系统(JVM动态分发,覆盖)会处理相关细节,保证程序正确的行为。

1.7 容器和泛型

  容器,就是Java中的数据结构了,不同的容器提供不同的接口和行为,对于某些操作具有不同的效率。在JDK 5 之前容器存储的对象是Obejct,存储对象必须向上转型,会丢失其身份,当取出时需要向下转型,可能会出错,因为不知道之前放进去的是什么类型的对象,因此,JDK5增加了泛型,明确指出容器可接收的对象类型。

1.8 对象的创建和生命周期、异常和并发

  Java 使用动态内存分配技术,使用关键词 new 在堆上创建对象。使用垃圾回收器释放对象占用的内存。

  Java 内置了异常的处理,而且强制使用。异常提供了一种从错误进行可靠恢复的途径。

  并发控制好共享资源的访问即可,Java提供了并发编程库,有现成可用的并发数据结构。

1.9 Java与Internet

  网络编程会涉及到很多知识,TCP/IP,多线程,IO模型等,要写出高性能的Java程序,还是要下大工夫的,虽然大问题被JVM搞定了。

第 2 章 一切都是对象

2.1 对象

  Java 通过引用来操作对象,使用 new 创建对象。那么对象被安置在哪个地方呢?计算中有5个地方可以存储数据,分别是:

  • 寄存器:位于CPU内部,最快的存储区,根据需求进行分配,不能直接控制
  • 堆栈:位于RAM,使用堆栈指针上下移动分配内存,速度仅次于寄存器,Java对象不在此,但引用在这里。Java系统知道栈里的元素确切生命周期
  • 堆:位于RAM,存放所有的 Java 对象(但也不绝对),分配内存比较灵活,但代价是慢
  • 常量存储:常量值通常放在程序代码内部,或者放在 ROM 只读存储器中
  • 非RAM存储:如将对象序列化到磁盘上,或者存在数据库中

2.2 基本类型

  基本类型存储值,并置于堆栈中,高效。Java 中基本类型的大小是固定的,不随硬件架构的变化而变化。基本类型如下:

  • char:16-bit,2字节,Unicode字符,0 ~ 2^16-1,Character
  • byte:8 bits,1字节,-128 ~ 127,Byte
  • short:16 bits,2字节,-2^15 ~ 2^15-1,Short
  • int:32 bits,4字节,-2^31 ~ 2^31-1,Integer
  • long:64 bits,8字节,-2^63 ~ 2^63-1,Long
  • float:32 bits,4字节,IEEE754,Float
  • double:64 bits,4字节,IEEE754,Double
  • boolean:true/false,Boolean
  • void:Void

  所有数值均有正负号,JDK5的自动包装功能,自动地将基本类型转为包装类型。

  高精度数字:BigInteger:支持任意精度的整数;BigDecimal:支持任意精度的定点数。

  数组也是对象,能存储基本类型和引用类型,Java会确保数组被初始化。

2.3 作用域 scope

  Java 使用花括号定义作用域,局部变量在花括号结束时,生命周期就结束了,而对象不是如此,它能一直保存下去,Java通过垃圾回收器管理对象的内存。一般不会出现内存泄漏,但也不是绝对的。

2.4 类,字段,方法

  使用 class 关键字定义一个类,类拥有字段(成员变量)和方法,对于成员变量,即使没进行初始化,Java也会保证它有一个默认值,引用类型默认为null,数字都默认为0,布尔默认false,char默认’u0000’(null)。对于局部变量编译器会强制进行初始化。

  方法,方法名和参数合起来称为方法签名,关于参数,在Java中只有值传递。Java消除了向前引用的问题,也就是同一个类中,成员变量和方法的先后顺序可以随意。

  static关键字可用来修饰字段,方法和类。修饰字段方法:表示属于类,不用新建对象就可使用。一般是修饰内部类,此类与一般的类没有差别。

2.5 注释

  常用标签和html如下:

  • @see:引用其他类,@see classname,@see fully-qualified-classnam#method-name
  • @author:作者信息
  • @version:版本信息
  • <pre>code</pre>:代码
  • <ol><li>1<li>2</ol>:列表
第 3 章 操作符

3.1 优先级&赋值运算符

  从左到右先乘除后加减,当不确定时,使用括号明确标识即可。

  赋值运算符(=),对基本类型赋值就是把一个地方的内容复制到另一个地方,比如int a=b,就是把b的内容复制给a;对对象赋值只是使这个变量也指向该对象,比如String s = a,s和a指向同一个对象。将对象传给方法时,也只是传递一个引用的值,或者说传递一个对象的别名。

3.2 算术、关系、逻辑运算符,直接常量

  加减乘除,取模,一元加、减操作符,自增,自减。

  == 作用于基本类型,比较值是否相等;作用于对象比较是否是同一个引用,比较对象使用equals,默认的equals比较引用,需要重写。

  与(&&)、或(||)、非(!)生成一个布尔值,具有短路功能,即如果第一个表达式能确定整个表达式的结果,那么就不会运算后面的表达式。

  直接常量,必须明确告诉编译器常量类型,比如10F,10D,10L,0xFF。对于char、byte、short超过其最大范围自动转为int。

  指数计数法:float a = 1.39E-43F;表示1.39×e^-43,如果不加F编译器默认会当做double处理,会提示类型转换。

3.3 位操作符和移位操作符

  位操作符:

  • 与(&):同为1,输出为1
  • 或(|):有1,输出为1
  • 异或(^):不全为1,输出为1
  • 非(~):取反

  移位操作符,只能用来处理整数,char、byte、short移位时自动转为int:

  • 左移(<<):低位补0,相当于乘以2^n,n为移动位数
  • 右移(>>):使用符号位扩展,为正高位补0,为负高位补1,相当于除以2^n,n为移动位数
  • 无符号右移(>>>):高位用0扩展

  在进行移位时,比如int只有数值右端的低5位才有用,比如 16>>2 和 16>>34 相等,因为2^5=32,相当于对32取模。long类型就是低6位数字有效。

  这里多说两句,源码中或进行移位时会经常看到(&0xFF),原因是什么呢?

  一般我们会对字节byte进行操作,首先 0xFF 表示低8位(1字节),当对byte移位操作时,会自动转成int,而Java中 int类型有32位,并且在计算机中数字使用有符号的二进制补码表示。所以byte转为int时会进行符号扩展,高位以符号位填充。如果byte为正数那么其补码与原码相同,此时进不进行位与操作都无所谓,但是为负数时不同,比如byte a = -4;其转为int在内部的表示就是11111111111111111111111111111100,这进行运算显然是不正确的,有效位只有低八位,所以与0xFF位与运算,把高24位置0,运算结果就正确了。

  不只是移位操作,只要byte或short转为int时都会出现此问题,此时只需进行位于运算取得有效数字即可。

3.4 三元操作符    boolean-exp ? true : false

3.5 字符串操作符 + 和 +=

  字符串连接操作符,编译器会把双引号内的字符序列自动转换成字符串,如果表达式中有字符串,表达式最终结果为字符串。

3.6 类型转换操作符 (cast)

  转换对象既可以是数值,也可以是变量。转换分为两种:窄化转换和扩展转换。窄化转换:编译器会强制进行类型转换,如把float赋值给int,此时数字会被截尾而不是四舍五入(使用Math.round()进行舍入)。扩展转换:不必显示的进行类型转换,编译器会自动处理,如把int赋值给float。

  Java 中,除了布尔类型,其他基本类型可以相互转换。类数据类型不能相互转换。

第 4 章 控制流程

while和do while的区别,即使表达式为false,do-while也会执行一次,而while不执行。

for循环,如 for(int i = 0; i < n; i++)。Foreach用于遍历数组和容器,如 for(String st :args) { }

死循环while(true) 和 for(;;),对编译器来说是一回事。goto一般不用。

switch语句,case因子支持能产生整数值得表达式和枚举,比如,byte、short、int、char,JDK7开始支持 String

第 5 章 初始化与清理

5.1 用构造器确保初始化

  创建对象时,JVM会自动调用该对象的构造方法,确保在使用前被正确初始化。Java 构造方法名称与类名称相同,没有返回值。无参构造方法,又称默认构造器,编译器会自动创建一个默认构造方法,但是如果写了一个构造方法,编译器就不再自动创建了,如果你添加的是有参数的,那么无参的构造方法就没了。

5.2 方法重载

  为了让方法名相同的方法存在,必须用到方法重载。区分方法重载的依据:根据类名和方法的形参列表。不能以返回值来区分。

  如果实参(char)小于形参(int),实参会被提升;如果大于,编译器会强制类型转换。

5.3 this 和 static

  this,表示当前对象的引用。this关键字只能在方法内部使用,编译器会“偷偷”把这个引用传到方法内部,在方法内部调用同类的另一方法或者使用同类的字段,不需要明确使用this,编译器会自动添加。

  在构造方法中调用其他构造方法,使用this可以实现,必须把this置于最起始处,并且只能调用一个(一次)。

  static方法就是没有this的方法,内部不能调用非静态方法,反过来可以。可通过类本身来调用static方法。

5.4 清理:终结处理和垃圾回收

  Java 中的对象:

  • 对象可能不被垃圾回收
  • 垃圾回收不等于析构
  • 垃圾回收只与内存有关

  垃圾回收和finalize()都不保证会一定发生,如果JVM并未面临内存耗尽的情形,它是不会浪费时间执行垃圾回收释放内存。

5.5 垃圾回收如何工作

  HotSpot VM 采用了分代回收的方法,其工作流程是,首先在年轻代中的Eden区移动”堆指针”为对象分配空间,当Eden满了或无法分配,进行一次垃圾回收,把还存活的对象复制到Survivor区或者提升到老年代,清空Eden,如此循环,如果老年代满了,对整个堆进行回收,清空年轻代。

  垃圾回收器就是通过一面回收空间,一面使堆中的对象紧凑排列,实现了一种高速,无限空间的堆模型。如何判断”活”对象?对任何”活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用,这个引用链条可能穿过数个对象层次。所以从堆栈或静态存储区开始,就能找到所有”活”的对象。

  • 引用计数:简单但速度慢,不能解决对象交互引用,没有应用于JVM实现
  • 停止-复制:将存活对象复制到另一块内存(如Survivor中的from和to两个内存块)
  • 标记-清扫:首先标记全部”活”的对象,完成后再清理,但剩下的堆空间不连续,需要重新整理
  • 分代回收:把堆分成不同的内存区域,不同区域使用不同的回收算法

  Just-In-Time,JIT,即使编译器,在必要时把热点代码翻译成本地代码。代码每执行一次就会做一些优化,执行次数越多,速度越快,所以,理论上Java 程序运行是越来越快的。

5.6 成员初始化、构造方法初始化、静态和非静态数据初始化

  对应方法的局部变量,使用前没有初始化,编译器以错误的形式保证初始化。对于数据成员(即字段)则不一样,JVM会保证每个字段有一个默认值,当然也可以指定字段的初始值。

  构造方法被JVM 自动调用,在类内部,变量初始化顺序由其定义的先后决定,并且在构造方法调用之前初始化。

  静态初始化只在必要时刻进行,其实这是因为JVM是按需加载类。静态数据先于非静态数据初始化。

5.7 初始化小结

  静态字段、块先于非静态数据初始化,顺序由其定义的先后决定;非静态字段、块先于构造方法初始化,顺序由其定义先后决定;构造方法最后被调用。如果考虑继承,和这差不太多。

5.8 数组初始化

  以int类型数组为例。

  int[] a[]; 数组定义

  int a[] = {1, 2, 3}; 定义并初始化,编译器会分配存储空间,相当于使用new

  int a[] = new int[] {1, 2, 3}; 定义并初始化,与上面的区别是,可以在其他地方手动初始化

  可变参数,如 print(int… args) 编译器会自动填充数组,所以获得的仍旧是一个数组参数。

5.9 枚举

  JDK5添加了枚举类型,enum,枚举可当做类来处理,并且具有自己的方法。枚举经常配合switch使用。

  public enum Database { MYSQL, ORACLE, MONGODB}

  switch(Database db)

  case MYSQL:

    // TODO

    break;

第 6 章 访问权限控制

6.1 创建独一无二的包名

  一个包(如jar文件)有许多.class 文件构成,为了更好的管理这里class文件,按照惯例,包名一般使用反转域名,利用操作系统的文件系统,把包名分解为机器上的一个目录。Java 解释器工作工程是:首先获得CLASSPATH环境变量,获取一个根路径,把包名转成路径名,然后在目录中查找class文件,当然也会去加载标准类库。CLASSPATH中有个点(.)表示当前目录。

  包的名称对应路径名称。Java 使用 package 组织类在单一的名称空间,通过导入不同的包,来解决冲突。

6.2 Java 访问权限修饰词

  Java中有4中访问权限,从大到小是:public、protected、包访问权限(默认权限)、private。

  访问权限的控制常被称为具体实现的隐藏,即封装,把数据和方法包含在类中,隐藏具体实现。其结果就是一个具有特征和行为的数据类型。访问权限控制将权限的边界划在了数据类型的内部,主要出于以下两种原因:

  • 设定客户端可以使用和不可以使用的界限:不必担心客户端将内部机制当成接口的一部分
  • 分离接口和具体实现:可以随意更改非public的东西,而不破坏客户端代码

  为了清楚可见,可以采用一种将public成员置于开头,后面是protected、包访问权限和private成员,站在类使用者的角度可以从头读起,关注对自己最重要的部分。可以使用eclipse的 source->Sort Members 功能自动排列。

第 7 章 复用类

  复用代码的两种方式:组合和继承。

7.1 组合语法

  组合即将类引用置于新类中,编译器自动将此引用置为null,可以在以下位置初始化:

  • 定义的同时进行初始化
  • 在构造方法中
  • 在使用之前初始化,称为惰性初始化,可减少额外的负担

7.2 继承语法、初始化

  Java 中类隐式地从java.lang.Object继承。可以使用 extends 关键字明确继承某类,会自动得到父类中所有的域和方法。Java 使用super 关键字表示父类,可以明确调用父类方法。

  从外部来看,子类就像是一个与父类具有相同接口的新类,或许还有一些额外的方法和域,但继承并不只是复制父类的接口。当创建一个子类对象时,该对象包含一个父类的子对象,这个子对象与用父类直接创建的对象是一样的。二者区别在于,后者来在于外部,而父类子对象被包含在子类对象的内部。

  为了保证父类子对象的正确初始化,Java 会自动在子类的构造方法插入对基类构造方法的调用,初始化过程是从父类”向外”扩散。

  如果父类没有无参的构造方法,那么就必须显示的使用 super 关键字调用父类的构造方法,并配以适当的参数列表,如 super(true)

7.3 代理

  如果一个类 A,想复用另一类 B 的接口方法,但不想使用继承怎么办?首先,先定义与 B 相同的方法名,然后在内部维持一个 B 的引用,方法的处理使用 B 的同名方法即可,这就称为代理。

7.4 名称屏蔽、protected、向上转型

  如果Java 的父类拥有某个已被多次重载的方法,那么在子类中重新定义该方法并不会屏蔽其在父类中的任何版本,也就是说重载机制可以正常工作。

  子类能够访问父类pulbic和protected的字段或方法,能覆盖重写,对于private类型的只有父类子对象能够访问。”覆盖”只有在某方法是父类接口的一部分时才会出现,如果某方法是private,它就不是父类接口的一部分,它仅是一些隐藏于类中的代码,如果子类有同名的方法,也只不过是具有相同名称而已。

  JDK5 增加了一个 @Override 注解,用以表明我要覆盖父类的方法,可以防止在不想重载时而意外进行了重载。

  子类是父类的一种类型,子类复制了父类的接口,子类可以向上转型为父类,以完成某些功能。

7.5 final 关键字

  final通常表示不可改变的,final使用的情况有三种:数据、方法和类。

  • final 数据:对于基本类型,数值不变;对于引用,引用不变,但引用的对象可能改变;对于参数,方法内部无法改变引用的指向,用于内部类传参,跟变量生命周期有关。final数据分为两种,一个是永不改变的编译时常量,一个是运行时初始化的值,之后不希望改变 P(140)
  • final 方法:阻止子类覆盖,类中private方法隐式指定为final,子类无法取用private方法,也就无法覆盖
  • final 类:此类无法被继承,无需做任何改动

7.6 初始化和加载

  类是按需加载的。初始化顺序为(其中内部按定义先后进行):

  • 先初始化父类静态数据和静态块,再初始化子类静态数据和静态块
  • 接着初始化父类非静态数据和块,然后调用父类的构造方法
  • 最后初始化子类非静态数据和块,调用子类的构造方法
第 8 章 多态

  在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。“封装”通过合并特征和行为创建新的数据类型。”实现隐藏”通过将细节私有化(private)分离接口和实现。而多态则是消除类型之间的耦合关系,通过分离做什么和怎么做,从另一角度分离接口和实现,能够改善代码的组织机构和可读性,能够创建可扩展的程序。

  多态也称为动态绑定、后期绑定或运行时绑定。

8.1 向上转型和方法绑定

  对象既可以作为它自己本身的类型使用,也可以作为基类型使用,而把对象的引用视为其父类型引用的做法就叫向上转型。那么已经转为父类型,程序又是怎么正确调用子类的方法呢?这和方法的绑定有关。

  首先,编译器是不知道对象的类型的,不能前期绑定。如果你查看字节码的话会发现,多态方法的调用是通过invokevirtual 指令调用父类的方法,在运行时怎么才能找到正确的对象呢?通过对象内部的vtable和itable信息就可以找到对象继承的类和实现的接口的方法。

  Java 中除了static 方法和final方法(private方法属于final方法)之外,其他所有方法都是后期绑定。

8.2 缺陷:”覆盖”私有方法

  私有方法默认是final方法,对子类来说是屏蔽的,所以如果子类有一个同名的方法话,只是相当于添加一个新方法,当向上转型时,会调用父类中的方法。子类中对于父类的private方法,最好采用不同的名字。

8.3 缺陷:域与静态方法

  静态方法不具有多态性,因为它是与类相关而非某个对象。

  不要在子类中定义与父类同名的字段(特别是那些私有方法和私有字段),容易令人混淆。

  但是也能解释的通,在分析运行过程时可以这样分析:

  • 每个对象的方法编译器都是传入一个当前对象的this引用,方法内部调用本对象的字段或方法都会默认加上this
  • 子类中会包含一个父类的子对象
  • 当把子对象向上转型时,当调用父类的私有方法时,方法内引用的是父类子对象;当调用父类protected或public时,如果子类有同名方法,多态会保证调用子类的方法,此时方法内部引用的是子类对象

8.4 构造器和多态

  构造方法不具有多态性,因为它默认是static的,隐式声明。

  涉及到继承的初始化顺序,首先父类肯定在子类之前初始化,然后再是子类。对于静态数据有些特殊,具体过程如下:

  • 父类静态代码 –>子类静态代码 –> 父类非静态代码 –> 父类构造方法 –> 子类非静态代码  –> 子类构造方法

  构造方法内部的多态行为,就是在内部调用可能被重写的方法,一般不会这么做,在构造方法被调用之前,字段会被赋予默认初始值。

  编写构造方法的准则:用尽可能简单的方法使对象进入正常状态,最好避免调用其他方法。构造方法唯一能安全调用的就是final方法,它不会被子类覆盖。

  协变返回类型:在子类覆盖的方法中可以返回父类方法的返回类型的某种子类型。比如 A extends B,C extends D,在D中有个方法 B fun() 返回B,在JDK5之前,子类C也只能返回B类型,而不能返回具体类型,有了协变就可以返回具体类型A。P(164)

8.5 用继承进行设计

  继承在编译期间就确定了对象之间的关系,我们不能再运行期间决定继承不同的对象,而组合可以在运行时改变对象的引用,动态选择类型(也就选择了行为)。一条通用的准则是:用继承表达行为之间的差异,并用字段表达状态上的变化。

  纯继承就是子类不添加新方法,与父类完全相同。

  向下转型与运行时类型识别:Java 中,所有的转型都会检查。

第 9 章 接口

  接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。

9.1 抽象类和抽象方法

  抽象类是普通类与接口之间的一种中庸之道,尽管在构建有有些未实现方法的类时,第一反应是新建接口,但抽象类仍是用于此目的的一种重要而必须的工具,因为你不可能总是使用接口。

  抽象类不能实例化,没有对象。没有抽象方法的抽象类,在阻止类实例化时有用。抽象类和抽象方法可以使类的抽象性更明确,并告诉用户编译器打算怎样使用它们,此外它们还是重构工具,把公共方法往上提。

9.2 接口

  接口是完全抽象的类,任何方法都没有具体实现,用来建立类与类之间的协议。Java 允许实现多个接口,类似于多重继承。

  接口中的域默认是public static final 的,方法默认是public的。

  使用接口和向上转型,可以实现解耦。策略模式-利用参数对象的不同,而表现不同的行为。适配器模式-使用组合来适配接口,或使用继承来适配。

  可以通过继承接口,来扩展接口,接口可以继承多个接口。

9.3 接口中的域

  默认是static final的,常用来做常量工具,JDK5之后又枚举类型。不建议使用接口纯粹的表示常量,可以使用final class 来表示,再来个private 的构造方法,更安全。

  接口中不允许空白final,但可以被常量表达式初始化。

9.4 嵌套接口

  接口可以嵌套在类或其他接口中,拥有public和包访问权限。P(186)

  使用工厂方法生成实现某接口的对象,为什么要添加额外级别的间接性呢?一个原因就是创建框架,对于复杂的代码可从中受益,也可使用匿名内部类来实现。

9.5 小结

  优先选择类而不是接口,当接口的必需性比较明显时,再进行重构。

第 10 章 内部类

10.1 内部类

  可以将一个类的定义放在另一个类的内部,这就是内部类。

  内部类不仅是一种代码隐藏机制,它了解外部类,拥有外部类所有成员的访问权限。内部类被创建时会秘密的捕获一个指向外部类对象的引用,注意,内部类对象只能与外部类对象有关联。

  使用 .this 可以获取外部类对象的引用。不能直接创建内部类对象,只能通过外部类对象使用 .new 创建。

  私有内部类,完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。比如 迭代器模式,Iterator。

10.2 在方法和作用域内的内部类

  可以在方法里面或在任意的作用域内定义内部类,有两个理由

  • 实现某类型的接口,创建并返回对其的引用
  • 创建一个辅助类解决问题,但希望此类不是公用的

  在方法的作用域内创建一个完整的类,称作局部内部类。比如在 a 方法中定义一个内部类 inner,此时标识符inner在方法结束仍可用,也就是说可以在同一子目录下的任意类中对某个内部类使用标识符inner,并不会有命名冲突。

  在语句块内,比如if语句中,此时不是说该内部类的创建是有条件的,它其实与别的类一起编译过了,只是在定义此内部类的作用域之外,它是不可用的,除此之外,它与普通的类一样。

10.3 匿名内部类

  返回一个没有名字的类,就叫做匿名内部类。匿名内部类末尾的分号,不是标记此内部类结束的,而是表示表达式的结束。如果内部类要使用一个在其外部定义的对象,并且在内部直接使用,那么编译器会要求其参数引用是final的,为什么呢?

  这和局部变量的生命周期有关,比如一个方法返回一个匿名内部类的引用,并传递了一个参数,方法结束此参数的生命周期结束,匿名内部类一般都包含回调,此时执行回调,就会找一个不存在的参数,而把参数使用final修饰,基本类型值不变,引用类型引用不变,保证了参数的一致性,此外编译器还会做一些手脚,会把此参数拷贝为匿名内部类对象的成员变量,这样就算局部变量声明周期结束,匿名内部类还是能够访问其副本。

  其实,不仅是匿名内部类,比如在静态块中的内部类,在方法中的内部类,等局部内部类,只要涉及到变量声明周期问题的,都需要使用final修改。final修改的变量是拷贝来用的,不是直接使用,它的不可变性也保证了一致性。

  对于匿名内部类而言,其字段初始化实际效果就是构造器,只不过没有名字,不能重载而已。匿名内部类既可以扩展类,也可以实现接口,但不能两者兼具,而且如果实现接口,只能实现一个。

10.4 嵌套类

  被声明为static类型的内部类,叫做嵌套类。嵌套类与普通内部类对象的区别:

  • 普通内部类对象保存了一个外部类对象的引用,能访问外部类对象所有成员
  • 而嵌套类对象与外部类对象关系不大,创建对象无需外部类对象,不能访问非静态的外部类对象
  • 普通内部类对象不能有static数据和字段,不能包含嵌套类
  • 而嵌套类可以包含这些东西

  接口内部的类,接口中的所有成员(字段 or 方法)默认都是 public static 的,那么内部的类就是嵌套类,其实嵌套类和普通类没有区别,只是存在的地方不同而已。

  一个内部类被嵌套多少层并不重要,它能透明的访问所有的外部类。

10.5 为什么需要内部类

  最主要的原因是,每个内部类都能独立地继承自一个(接口的)实现,无论外部类是否继承了此(接口的)实现,完善了多重继承。

  内部类的其他特性:

  • 内部类可有多个实例,每个实例都有自己的状态信息,与外部类对象相互独立
  • 单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类
  • 创建内部类对象的时刻并不依赖于外部类对象的创建,也就是说,什么时候创建内部类对象是自由的,比如Iterator可以在使用的时候再创建
  • 内部类就是一个独立的实体

10.6 闭包与回调

  谈到闭包,想到最多就是js中的闭包,在js中,在一个函数内定义另外一个函数就会产生闭包,就是指有权访问另外一个函数作用域变量的函数。而在Java中内部类就是面向对象的闭包。

  回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。回调的价值在于它的灵活性,可以在运行时动态的决定需要调用什么方法。

10.7 内部类的继承、是否可被覆盖

  在内部类构造器中使用使用enclosingClassReference.super()。

  不能覆盖,如果继承它的外部类,并且定义一个同名的内部类,不会影响另一个,也就是说,两个内部类是完全独立的两个实体,各自在自己的命名空间内。

  内部类标识,如果是匿名内部类编译器简单的产生一个数字作标识符;如果是嵌套在其他类中,则使用外部类名字加$加内部类名字。

第 11 章 持有对象

  数组是编译器支持的类型,能够保存一组对象,不足之处是容量固定,一般写程序时并不知道需要多少对象,Java类库提供了一套相当完整的容器类来解决这个问题,基本类型是List、Set、Queue、Map,这些对象又称为集合类。容器有各自的特点,并且大小都可自扩展。

11.1 泛型和类型安全的容器

  以ArrayList为例,在没有泛型之前,ArrayList存储的是Object对象,Object是所有类的直接或间接父类,也就是说可以将任何对象放进ArrayList中,在编译期或运行时都不会有问题,但是,当使用get方法取出对象时,得到的只是一个Object引用,必须强制转型为实际类型,否则会得到语法错误。

  比如有两个类,Apple和Orange,除了都是Object子类,两个类毫不相关。分别将Apple对象和Orange对象放进Arraylist中,此时通过get得到一个引用,我认为是Apple对象,然后强转为Apple类型,使用Apple对象提供的服务,但是当我拿到的引用实际上是Orange对象呢?在运行时会抛出一个类型转换异常,Apple cannot cast to Orange。

  使用泛型就能避免此问题,ArrayList<Apple>表示此容器存储的是Apple对象,这样在编译期防止将错误类型放到容器中。当使用get方法时会自动进行类型转换。向上转型也可作用于泛型,即父类及其子类能放在同一个容器中。(泛型远不止保证容器类型安全这么简单.)

11.2 基本概念

  容器的用途是“保存对象”,其中有两种不同的概念:

  • Collection,一个独立的元素序列,List按插入顺序保存对象,Set不能有重复元素,Queue按照队列规则确定顺序。
  • Map,一组“键值对”对象,也称关联数组。

11.3 添加一组元素

  java.util包中的Arrays和Collections类中有很多实用的方法,简单整理如下:

(1)Arrays类

  • Arrays.sort(..),对数组进行排序,自JDK7内部使用 DualPivotQuicksort 快速排序算法
  • Arrays.parallelSort(..),并行排序,JDK8,大数据量时性能较好
  • Arrays.binarySearch(..),二分查找
  • Arrays.fill(..),指定值填充数组
  • Arrays.copyOf(..),拷贝指定数组,返回一个新数组对象
  • Arrays.toString(..),打印数组
  • Arrays.asList(..),可变参数列表,返回一个ArrayList对象,底层是数组,不能调整大小,add或delete会抛出不支持操作异常。Arrays.<Type>asList(..)显示说明类型参数,具体原因详见P221

(2)Collections类

  • Collections.addAll(..),往一个容器中添加一组对象
  • Collections.sort(List),对一个list排序
  • Collections.synchronized..,将一个容器包装成线程安全的
  • Collections.shuffle(..),随机打乱一个列表的顺序
  • Collections.toArray(..),将一个列表转为数组

  容器的toString方法都提供了可读性良好的输出结果。

11.4 容器

(1)List接口在Collection接口之上,添加了大量方法,可在List中间插入和删除元素。有两种类型的List:

  • ArrayList:随机访问快,在List中间插入删除元素较慢,使用数组实现,默认大小为10
  • LinkedList:在List中间插入和删除较快,随机访问较慢

(2)迭代器

  迭代器是一种设计模式,它是一个轻量级对象,用来遍历容器中的对象,不必关心容器底层的结构。Java的Iterator只能单向移动,只能用来:

  • 使用方法 iterator() 要求容器返回一个Iterator,准备返回容器第一个元素
  • 使用 next() 获取下一个元素
  • 使用 hasNext() 检查是否还有元素
  • 使用 remove() 将迭代器新近返回的元素删除,即移除 next产生的最后一个元素,调用remove()之前必须先调用next()

  为什么需要迭代器呢?不必关系容器中元素的个数;不关心容器类型,代码更通用。迭代器统一了对容器的访问方式,将遍历容器的操作与容器底层的结构分离。

  ListIterator 是一个更加强大的Iterator的子类型,只能用于各种List类型的访问,可以双向移动。可以产生相对于迭代器在列表中的前一个和后一个元素的索引,并且可使用set方法访问过的最后一个元素。通过调用listIterator获得一个指向List开始处的ListIterator对象,此外可使用listIterator(n)指定指向的初始位置。

(3)LinkedList

  实现了List接口,添加了可以使其作为栈、队列或双端队列的方法。

  getFirst(),element()返回列表的头,为空时抛出异常,peek方法与这俩方法稍有差异,列表为空时返回null。

  removeFirst()与remove()方法一样,为空时抛出异常,poll为空时返回null。

  addFirst(),add(),addlast()三个方法相同,都将某个元素插入到表尾。removeLast()移除并返回列表的最后一个元素。

(4)其他

  • Stack,栈,后进先出的容器,LinkedList具有能够直接实现栈的所有功能的方法,可以使用LinkedList自定义一个stack的实现
  • Set,保存重复的元素,与Collection具有一样的接口,只是表现行为不同。HashSet输出顺序无规律可循,因为使用了散列。TreeSet将元素存储在红-黑树数据结构中元素有序。HashSet使用了散列函数,LinkedHashSet也使用了散列函数,使用了链表来维护元素的插入顺序。
  • Map,将对象映射到其他对象,将基本类型放进HashMap时,会发生自动包装和拆包。Map和Collection一样,可以很容易扩展到多维,如Map<String, List<Integer>>
  • Queue,队列是一个先进先出的容器,队列在并发编程中特别重要。PriorityQueue 优先级队列,描述队列进出规则。
  • foreach可用于数组,也可用于容器。foreach其实是使用Iterable接口来在序列中移动。也就是说只要实现了Iterable接口的类型都可用于foreach语句中。
第 12 章 通过异常处理错误

  Java 的基本理念是“结构不佳的代码不能运行”。

12.1 基本概念

  Java异常机制的好处:

  • 编译器强制执行异常处理,利于构建大型、健壮、可维护的程序
  • 降低错误处理代码的复杂度,可以选择在调用处处理,也可将异常抛出到另一个地方处理,分离了“正常过程中做什么事”和“出了问题做什么事”的代码

  抛出异常时,首先,使用new在堆上创建异常对象(GC会回收);然后,当前的执行路径终止,并从从当前环境中弹出异常对象的引用;此时异常处理程序(catch块)处理异常。

  所有异常都有两个构造函数,一个默认的和一个以字符串为参数的,放异常相关信息。

  使用 try 捕获异常,catch 处理异常,catch会依次向下搜索与异常匹配的第一个处理程序,一旦执行完毕,则异常处理完毕,只有匹配的catch块才会被执行。

  继承Exception,自定义异常,让编译器自动产生默认的构造函数即可,异常的名称应该能望文生义。

  throws 关键字可以在方法名称说明潜在的异常列表;throw 关键字在代码中抛出具体的异常。

  RuntimeException 运行时异常,也称为不受检查的异常,这类异常不需要在程序中显式的捕获,因为它代表的是编程错误,如NullPointException、ClassCastException、ArrayIndexOfBoundException…

  受检查的异常,编译时被强制检查的异常,如IOException。可通过throw new RuntimeException(cause),转为不检查异常。

12.2 异常

  catch(Exception e) { e.printStackTrace(); } 捕获所有异常,并打印异常调用堆栈信息,默认输出到System.error,可使用其他构造方法进行重定向。getStackTrace()方法返回栈轨迹元素数组,每一个元素表示一个栈帧,元素0-栈顶元素是最后一个方法调用,最后一个元素是第一个方法调用。

  重新抛出异常,即把刚捕获的异常重新抛出,使用throw 关键字。如果只是抛出当前异常对象,那么printStackTrace()方法显示的是原来异常抛出点的调用栈信息,而非重新抛出点的信息。可使用fillInStackTrace()方法把当前调用栈信息填入原来异常对象,并返回一个Throwable对象,调用fillInStackTrace()方法的地方就成了异常的新发地。如果抛出另一个异常对象,效果类似于调用fillInStackTrace,但是原来异常的信息会丢失。

  捕获一个异常后抛出另外一个异常,并且保存原始异常信息,此为异常链。有两种方式:一是使用Throwable(Throwable cause)构造方法传递原始异常;二是使用initCause(Throwable cause)链接异常。其中只有Error、Exception、RuntimeException有带cause参数的构造方法,其他异常使用initCause方法链接。

12.3 使用 finally 清理

  finally子句用来做一些清理工作,比如打开的文件或网络连接。finally块总是被执行:

  • 不管 try 块有没有发生异常
  • 就算 catch块捕获异常并且使用 throw 再次抛出
  • 使用 break或continue时,也会执行
  • 哪怕 return 仍旧执行

  为了防止异常丢失,要把所有可能抛出异常的方法放到try-catch子句中。

12.4 异常限制

  当覆盖方法时,只能抛出在基类方法的异常说明里列出的那些异常。异常限制对构造方法不起作用,子类构造方法不能捕获父类构造方法抛出的异常。

  编写构造方法时不要做太多处理,一个原则就是尽快让对象进入一个稳定状态。

第 13 章 字符串

  字符串操作时程序设计中最常见的行为。

13.1 String

  String 是不可变对象,对其进行的操作均不会影响原有的字符,而是返回一个全新的String对象。使用”+”连接字符串,大量的连接会产生一堆垃圾中间对象,虽然编译器会自动优化,但最好的方式是使用StringBuilder.append,不要使用append(“a”+”b”),编译器会创建另一个StringBuilder处理括号内的字符串操作。

  无意识的递归,当打印类信息时,在toString()方法中使用this关键字,其实this默认调用toString()方法。

  String上的常用操作(下标从0开始):

  • length():字符个数
  • substring(int begin):从 begin 位置开始到字符结束的子串
  • substring(int begin, int end):从 begin 位置开始到 end-1 位置的子串,子串长度为 end-begin
  • trim():删除两端空白字符,返回新String对象;若没有改变,返回原对象
  • intern():如果常量池中存在当前字符串, 直接返回当前字符串;如果常量池中没有此字符串, 则将此字符串放入常量池中后, 再返回

  为了节省内存和提高运行速度,对于基本类型和String类型,Java提供了常量池的概念,对于String,直接使用引号会直接存在常量池中;不使用引号的String对象,可使用intern方法,尝试将当前字符放入常量池。比如在JDK7中:

  • String s0 = new String(“1”); 生成两个对象,堆上new一个对象s0;常量池对象“1”
  • s0.intern(); 尝试放进常量池,(equals比较)已存在返回
  • String s1 = “1”; 指向常量池对象“1”
  • s0 == s1; false
  • ========================
  • String s0 = new String(“1”)+new String(“1”);  最终生成两个对象,堆上new一个对象s0,内容为“11”;常量池“1”对象,注意是1
  • s0.intern(); 把“11”放进常量池,常量池不存在“11”,此时常量池对象“11”引用s0所指对象
  • String s1 = “11”; 指向常量池对象“11”
  • s0 == s1; true

13.2 格式化输出

  和C的printf()风格一样的格式化输出,Java中格式化功能都由java.util.Formatter类提供。构造Formatter实例时可以指定结果输出目的地,如Appendable(StringBuilder)、OutputStream、File。

  格式化说明符,用来控制空格与对齐。%[argument_index$][flags][width][.precision]conversion,说明如下:

  • argument_index$:指示参数列表中参数位置,如1$,2$等
  • flags:修改输出格式的字符,取决于conversion,如 ‘-’左对齐,‘0’使用0填充,%02X 十六进制格式
  • width:指定一个域最小尺寸,作用于字符串,如 %5s,字符串长度最小为5,不足填空格,右对齐,%-5s左对齐
  • precision:指定一个域最大尺寸,作用于浮点数限制小数位数,舍入或补0 ,如%5.2f 结果长度最少5不足补0,小数点最多两位
  • conversion:类型转换字符,d(十进制整数),c(C,Unicode字符),b(B,Boolean值),s(S,String),f(十进制浮点数),e(E,浮点数科学计算),x(X,十六进制整数),h(H,十六进制散列码),%(字符%),t(T,格式日期的前缀)

        String.format()方法,内部会新建一个Formatter对象,多次调用不要使用此方法。

      13.3 正则表达式

        强大而灵活的文本处理工具。解决字符串匹配、选择、编辑以及验证问题。Java 中对象反斜杠 的处理,\ 表示插入一个正则表达式的反斜杠,也就是其后的字符有特殊意义或者用来转义,如 \d 表示一位数字,\\ 表示普通的反斜杠,\+ 表示字符+。

        String对象提供了丰富的字符串操作,如果正则表达式不止使用一次的话,非String对象的正则表达式具备更加的性能。虽是这么说,但写出高性能的正则表达式有难度,不一定比API快。

        特殊字符: 制表符, 换行符, 回车,f换页,e转义(Escape)

        预定义类:.(点)任意字符;[abc] 包含a,b,c任一字符和a|b|c相同;[^abc] 除了abc之外的字符;[a-zA-Z] a到z或A到Z的字符;[abc[hij]] 任意abchij字符;[a-z&&[hij]] 任意hij字符;s 空白符(空格、tab、换行、换页、回车);S 非空白字符[^s];d 数字[0-9];D 非数字 [^0-9];w 词字符[a-zA-Z0-9];W 非词字符[^w]

        逻辑操作符:XY Y跟在X后面;X|Y X或Y;(X) 捕获组,可在表达式中使用i引用第i个捕获组

        边界匹配符:^ 一行的起始;$ 一行的结束;B 非词的边界; 词的边界;G 前一个匹配的结束

        量词:X? 一个或零个;X* 零个或多个;X+ 一个或多个;X{n} 恰好n个;X{n,} 至少n个;X{n,m} 至少n个但不超过m个

        量词表达式分为:贪婪型,为所有可能得模式进行匹配,默认;勉强型,满足模式的最少字符,使用?;占用型,防止匹配失败回溯,使用+。表达式中的X通常使用圆括号括起来(X)。

        组(Groups),用括号划分的正则表达式,可以根据组号引用某个组,组号0为整个表达式,组号1表示第一对括号括起的组,以此类推。如 A(B(C))D ,其中有三个组,组0为ABCD,组1为BC,组2为C。

      13.4 Pattern 和 Matcher

        java.util.regex包提供了更加强大的正则表达式对象。

        Pattern.complie(String regex) 和 Pattern.complie(String regex, int flag)方法编译正则表达式,使用matcher()方法生成一个Matcher对象。其中 flag 可以调整匹配的行为,常用的常量有:

      • Pattern.CASE_INSENSITIVE,默认大小写不敏感的匹配限定,US-ASCII字符集,配合UNICODE_CASE结合此标记,可以对Unicode字符集实现大小写不敏感的匹配;
      • Pattern.DOTALL,此模式下,点匹配所有字符,包括行终结符,默认不匹配;
      • Pattern.MULTILINE,多行模式,^和$分别匹配一行的开始和结束,还匹配字符串的开始和结束,默认只匹配完整字符串的开始和结束;
      • Pattern.COMMENTS,忽略空格符和#开始的行。

        多个 flag 使用 | 分割,如Pattern.complie(“^java”, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE),匹配以 Java 开头的行,不区分大小写,对每一行(从字符序列的第一个字符开始,到每一行终结符)都进行匹配。

        Matcher对象,有以下几种方法:

      • matches():判断整个输入字符串是否匹配
      • lookingAt():判断输入字符起始部分是否匹配
      • find():查找输入字符串中的多个匹配
      • group():获得整个表达式匹配结果,group(1)获得第一个组匹配结果
      • start(),end():返回先前匹配的字符串开始索引和最后索引加1的值
      • reset():将现有的Matcher对象应用于一个新的字符串

        替换操作,replaceFirst()、replaceAll()替换时只能使用一个固定的字符串,appendReplacement(buf)执行渐进式的替换,可以对匹配到的字符串做一些额外的操作,如转成大写,appendTail()将剩余的部分追加到buf中。

      13.5 扫描输入

        Scanner 类,可以接受任何的输入对象,包括File、InputStream、String或Readable对象,使用hasNext()和next() 遍历。Scanner会把IOException吞掉,可使用ioException()方法,找到最近发生的异常。

        Scanner 默认使用空格作为定界符,可使用useDelimiter()设置定界符。其他用法可查看API文档。

        StringTokenizer,有了正则和Scanner这个类可以不用了。

      第 14 章 类型信息

      14.1 RTTI

        运行时类型信息(RTTI,Runtime Type Information)使得可以在程序运行时发现和使用类型信息。

        Java 有两种方式让我们在运行时识别对象和类的信息:一是假定在编译时已经知道了所有的类型;二是使用“反射”机制。

        运用多态时,子类会向上转型为父类,丢失了具体类型,客户端拿到了一个泛化的引用,而使用RTTI可以查询某个泛化引用的确切类型。

      14.2 Class 对象

        Class 对象就是一个类在运行时在 JVM 中的表示,JVM 按需加载类,使用Class.forName(“className”) - 全限定名,手动触发加载,获得对象的引用。

        CLass.getInterfaces() 获取对象接口信息;Class.getSuperclass() 获取父类信息;Class.newInstance() 创建一个对象,尽管不知道它的确切类型,此类必须有无参构造方法。

        获得CLass对象的引用的另一种方法是使用类字面量,如String.class,简单,安全,高效,编译时就会检查,不必使用try-catch块。

        类字面量可应用于普通的类、接口、数组、基本数据类型及其包装类型。包装类型还可使用.TYPE,但最好使用.class。使用”.class”创建Class对象引用时,不会自动初始化该对象,即不会触发以下动作:加载,查找字节码创建Class对象;链接,验证字节码,为静态域分配空间,解析符号引用;初始化,若有父类,则对其初始化,执行静态初始化块。

        使用static final 修饰的字段,如果在编译阶段确定了具体值(编译期常量),那么直接引用不会触发类的初始化。一个static如果不是final的,那么在使用前先进行链接和初始化。

        泛化的Class引用。Class引用指向的就是对象的确切类型,可通过泛型语法,让编译器强制执行额外的类型检查。常用的是使用”?”和“?extends Class”,如Class<?> clazz = Number.class,Class<? exnteds Number> clazz = int.class。

      14.3 类型转换前先做检查

        强制类型转换,(Shape) 运行时如果转换错误,会抛出ClassCastException。

        instanceof ,Class.isInstance(Object obj) 判断对象是否是某个类的实例,指的就是你是这个类吗,或者你是这个类的派生类吗?

      14.4 反射:运行时的类信息

        反射可以用来检查可用方法和方法名,可以跨网络远程平台创建和运行对象,即RMI - 远程方法调用。Class类和java.lang.reflect包提供了对反射的支持,通过API匿名对象的信息能在运行时完全确定下来,而在编译时不需要知道任何事情。

        RTTI 与反射的区别在于:RTTI 在编译时打开和检查.class文件;反射在运行时打开和检查.class文件。

        动态代理,Java可以通过Proxy.newProxyInstance()和InvocationHandler来实现动态代理。

        空对象,使用内置的null,每次使用时都要判断是否为空,而null除了在你试图用它执行任何操作时产生NullPointerException,没有其他任何行为。可以引入空对象的思想,它接受它所代表的对象的消息,返回一个实际上不存在任何真实对象的值,可避免不必要的精力去检查null。

        接口与类型信息,通过反射能够得到任何你想要的信息,关于一个类。

      第 15 章 泛 型

        一般的类和方法只能使用具体的类型:基本类型和自定义的类。如果编写可应用在多种类型的代码,此限制对代码的束缚比较大。

        多态算是一种泛化机制,但依赖继承和接口;泛型实现了参数化类型,使代码应用于多种类型,Java泛型的核心概念就是告诉编译器想使用的类型,编码器会自动检查类型的正确性和自动转型。

      15.1 泛型

        泛型类和接口,只需在类名或接口名后添加泛型类型即可,如Stack<T>,Generator<T>,基本类型无法作为类型参数,会自动打包拆包。

        泛型方法,能够独立于类而产生变化,static方法需要使用泛型能力,必须使其成为泛型方法。定义泛型方法只需将泛型参数,如public <T> void fun(T x){ };  

        当使用泛型类时,需要在创建对象的时候指定类型参数的值,而调用泛型方法时,通常不需要指明参数类型,编译器会自动找出具体的类型,此为类型推断。类型推断只在赋值操作中有效,如果不是赋值操作,编译器不执行类型推断,此时编译器认为,泛型方法的返回值赋值给一个Object类型变量。

        JDK7之后,可以这样声明一个容器,Map<String, String> maps = new HashMap<>();

        在泛型方法中可以显式的指明类型,在点操作符和方法名之间插入尖括号,然后把类型置于尖括号内。如 New.<Person, List<Pet>>map(); P363

        泛型方法和可变参数列表能很好的共存:如 public static <T> List<T> makeList(T… args){ };

      作 者:创心coder
      QQ群:361982568
      订阅号:cxcoder
      文章不足之处,还请谅解!本文会不断完善更新,转载请保留出处。如果帮到了您,烦请点赞,以资鼓励。
      原文地址:https://www.cnblogs.com/cwane/p/6183682.html