java核心技术(卷一)

一,java基本程序设计结构:html

 

1,在网页中运行的 Java 程序称为 applet。 要使用 applet ,须要启用 Java 的 Web 浏览器执行字节码java

2,jdk安装目录下的 src.zip 文件中包含了全部公共类库的源代码。 要想得到更多的源代码 ( 例如 :编译器 、 虚拟机 、 本地方法以及私有辅助类 ),请访问网站 :http://jdk8.java.net。程序员

3, 浮点数值不适用于没法接受舍入偏差的金融计算中。例如,命令System.out.println(2.0-1.1)将打印出0.8999999999999999,而不是人们想象的0.9。这种舍入偏差的主要缘由是浮点数值采用二进制系统表示,而在二进制系统中没法精确地表示分数1/10。这就好像十进制没法精确地表示分数1/3—样。算法

4, 在Java中,-共有8种基本类型(primitivetype),其中有4种整型【byte 1个字节,short 2个字节,int 4个字节,long 8个字节】、2种浮点类型【float 4个字节,double 8个字节】、1种用于表示Unicode编码的字符单元的字符类型char和1种用于表示真值的boolean类型。基本类型和引用类型都保存在栈中,可是基本类型保存的是实际值,而引用类型保存的是一个对象的内存地址基本类型是内建在Java语言中的特殊的数据类型,它们不是继承自Object对象,因此int等基本类型不属于Object 【参考1】【参考2:官方教程说明】。日常Object o = (int) 3;不会报错,这是用了自动装箱功能。可是泛型中类型参数不能为基本类型,由于编译器类型擦除时会把泛型类型参数(假设此类型参数没有边界)设置为Object,而Object不能用于存储基本类型的值(没有用自动装箱功能)shell

4.1,float类型的有效位数(精度)为6~7位。double类型的有效位数为15位。json

5,码点(code point)表示 与一个编码表(如Unicode编码)中的某个字符对应的代码值。在Unicode编码表标准中,码点采用十六进制书写,并加上前缀U+,例如 U+0041 就是拉丁字母 A 的码点。Unicode的码点能够分为17个代码平面(code plane)。第一个代码平面,有时叫第零个代码平面,叫作 基本多语言平面(basic multimultilingual plane),码点从U+0000 到 U+FFFF。其他的16个平面从U+10000 到 U+10FFFF。 第一个平面里包含经典的Unicode代码,其他16个包括一些辅助字符。 UTF-16是Unicode的一种使用方式,UTF即Unicode Transfer Format,即把Unicode转换为某种格式的意思。UTF-16编码采用不一样长度的编码来表示全部的Unicode码点。在Unicode的基本多语言平面中,UTF-16将Unicode中的每一个字符用2个字节16位来表示,一般被称为 代码单元(code unit,又称码元)。而对于其余16个平面中的辅助字符,UTF-16采用一对连续的代码单元进行编码,即用2个(2字节的)码元表示。为了可以区分出某个码元是一个字符的编码(基本多语言平面中的字符,即单16位)仍是一个辅助字符(即双16位)的第一或第二部分,UTF-16编码规定以54开头(110110)的一个码元表示辅助字符的前16位即第一部分,以55开头(110111)的一个码元表示辅助字符的后16位,即第二部分。其余开头的码元则是单16位的表示字符的码元。因为第零平面的字符有0x0000-0xffff共65536个字符,恰好能够用16位表示完,如此确定也有以54开头的单16位编码。实际上,Unicode为了配合UTF-16规定了 以54开头的区间(即110110 开头的16位区间,范围从D800-DBFF,共1024个字符位置),和以55开头的区间(范围从DC00~DFFF共1024个字符位置)不容许分配任何字符。因此实际上Unicode第零平面表示的字符共65536-2048 个。参考文章:https://blog.csdn.net/wusj3/article/details/88641084。 Java中的char类型描述了UTF-16编码中的一个码元,一个码点可能包含一个码元也可能包含2个码元(例如: 𝕆 ,𠠦)。数组

5.1, Unicode字符编码表其实和计算机没有任何关系,它只是给每一个字符一个数字编号。如何在计算机中存储这些数字才是计算机的事情。有好多种实现方式,utf-8,utf-16等。其中,在Unicode的第零个平面中的字符(65536-2048个字符)其正常的二进制编码 和 这些字符使用 utf-16 编码后的结果是同样的。浏览器

6,const是Java保留的关键字,但目前并无使用。在Java中,必须使用final定义常量。缓存

7,整数被0除将会产生一个异常,而浮点数被0除将会获得无穷大或NaN结果。安全

8,在默认状况下,虚拟机设计者容许对中间计算结果采用扩展的精度。可是,对于使用 strictfp 关键字标记的方法必须使用严格的浮点计算(即中间结果要进行截断)。

9,在Math类中,为了达到最快的性能,全部的方法都使用计算机浮点单元中的例程..若是获得一个彻底可预测的结果比运行速度更重要的话,那么就应该使用StrictMath类,,它使用“自由发布的Math            库”(fdlibm)实现算法,以确保在全部平台上获得相同的结果

10,基本类型之间的转换:如图,

一,
    int n=123456789; float f = n; // f=1.23456792E8。 float类型的精度是6-7位。123456789包含的位数比float的精度要多,因此会损失必定的精度。 二, 两个基本类型的数值进行二元运算时,java编译器会先将两个操做数转换为同一种类型,而后再进行计算。 若是有一个操做数为double,另外一个也会被转换为double, 不然,若是有一个为float,另外一个也会被转换为float, 不然,若是有一个为long,另外一个也会被转换为long. 不然,两个操做数都会被转换为int。 精度小于int类型的数值运算会被自动转换为int类型而后再运算。如, 两个short类型的数值进行运算时,会首先将short类型转换为int。因此,以下代码编译会报错: short s1 = 1; short s2 = 1; s1 = s1 + s2;// 报错:没法将int类型赋值给short类型!  必须使用强制类型转换(cast): s1 = (short) (s1 + s2); 可是 s1 += s2;不会报错,由于 += 运算符在运算后(s1+s2),若是获得的值的类型与左侧操做数(s1)的类型不一样,就会发生强制类型转换: 
   即s1+=s2最终其实是:s1 = (short) (s1+s2)。 三, 在必要的时候,int类型的值会自动的转换为double类型。有时候须要将double类型转为int类型(这种转换会损失精度),在Java中这种操做不会自动进行,
须要经过强制类型转换(cast)实现这个操做。如:double x = 9.997; int nx = (int) x;//nx = 9; int nx = (int) Math.round(x);// nx = 10;

 

11,Java没有内置的字符串类型,而是在标准Java类库中提供了一个预约义类,很天然地叫作String。每一个用双引号括起来的字符串都是String类的一个实例。因为不能修改Java字符串中的字符,因此在Java文档中将String类对象称为不可变字符串.不可变字符串却有一个优势:编译器可让字符串共享。为了弄清具体的工做方式,能够想象将各类字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。若是复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

12, Java中比较字符串是否相等不能使用 ==。由于这个运算符只能肯定两个字符串是否放在同一个位置(这句话的含义实际是 == 比较字符串不只比较字面是否相同,还比较两个字符串的内存地址是否相同!)。若是java虚拟机始终将相同的字符串共享放在同一个内存地址中,那么就能够使用 == 检测字符串是否相等。可是实际上,只有字符串常量是共享的(即放在同一块内存中),而使用 + 或 substring等操做产生的结果并非共享的。因此千万不能使用 == 比较字符串是否相等。例如,String s = "Hello";  s.substring(0,2) == "He" 是错误的,二者并不 ==,可是倒是equals的。

12, String API

1,nothing to note

13,数组: 一旦建立了数组 ,就不能再改变它的大小( 尽管能够改变每个数组元素 )。

一,

  •  int[] arr = new int[10] ; arr[0] = 3;arr[1]=4;
  •  int[] arr = {1,2,3,4};
  •  int[] arr = new int[] {1,2,3,4}

二,

  • String arrStr = Arrays.toString(arr); 
  • int[] arr1 = arr; // 两个变量引用同一个数组,一个改变会影响另外一个
  • int[] arrCopy = Arrays.copyOf(arr, arr.length * 2); // 只拷贝值。若是数组元素是数值型,那么多余的元素将被赋值为0;若是数组元素是布尔型,则将赋值为false。相反,若是长度小于原始数组的长度,则只拷贝最前面的数据元素。
  • int[] arrCopy = Arrays.copyOfRange(arr, startIndex, endIndex); // [start, end) 
  • void Arrays.sort(arr);
  • boolean Arrays.equals(arr1, arr2);
    • //若是两个数组长度相同,而且在对应的位置上数据元素也均相同,将返回true。数组的元素类型能够是Object、int、long、short、char、byte、boolean、float或double
  • int[][] arrArr = new int[2][3];// {{1,2},{2,3}}
  • String arrArrStr = Arrays.deepToString(arrArr);

三,

第四章,对象与类:

※,如下都以以下2个类为例子:Employee, Manager

class Employee
{
   private String name; private double salary; private LocalDate hireDay; public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } public String getName() { return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
class Manager extends Employee
{
   private double bonus; public Manager(String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double b) { bonus = b; } }

 

14,能够显式地将对象变量设置为 null, 代表这个对象变量目前没有引用任何对象 。全部的Java对象都存储在堆中。当一个对象包含另外一个对象变量时,这个变量依然包含着指向另外一个堆对象的指针。

15,注意,在这个示例程序中包含两个类:Employee类和带有public访问修饰符的EmployeeTest类。EmployeeTest类包含了main方法。源文件名是EmployeeTest.java,这是由于文件名必须与public类的名字相匹配在一个源文件中,只能有一个公有类,但能够有任意数目的非公有类。接下来,当编译这段源代码的时候,编译器将在目录下建立两个类文件:EmployeeTest.class和Employee.class将程序中包含main方法的类名提供给字节码解释器,以便启动这个程序:javaEmployeeTest字节码解释器开始运行EmployeeTest类的main方法中的代码。

16,多个源文件的使用

一个源文件能够包含了两个类。许多程序员习惯于将每个类存在一个单独的源文件中。例如,将Employee类存放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。
若是喜欢这样组织文件,将能够有两种编译源程序的方法。一种是使用通配符调用Java编译器:
javac Employee*.java
因而,全部与通配符匹配的源文件都将被编译成类文件。或者键人下列命令:
javac EmployeeTest.java
读者可能会感到惊讶,使用第二种方式,并无显式地编译Employee.java,然而,当Java编译器发现EmployeeTest.java使用了Employee类时会查找名为Employee.class的文件。若是没有找到这个文件,就会自动地搜索Employee.java,而后,对它进行编译。更重要的是:若是Employee.java版本较已有的Employee.class文件版本新,Java编译器就会自动地从新编译这个文件。

17,p127: getter访问器方法注意不要返回 “可变对象”。由于对这个对象调用更改器方法会改变对象的私有状态,这是咱们不想要的。若是须要返回一个可变对象的引用,应该首先对它进行克隆,

18,p129: final 实例域。final关键字通常用于基本类型的域(即类的字段或称属性),或不可变类的域(若是类中的每一个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。final通常不用于可变的类,容易引发读者的理解混乱,例如:

private final StringBuilder evaluations ;
在 Employee 构造器中会初始化为
this.evaluations = new StringBuilder() ; final关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其余 StringBuilder对象。不过这个对象能够更改: public void giveGoldStar() { evaluations . append ( LocalDate . now ( ) + " : Gold star ! \ n " ) ; }

19,静态域与静态方法:

※,静态域、静态方法属于类不属于对象(或称为实例),因此静态方法中不可调用实例域,也不可调用实例方法。可是反过来,实例(或实例方法)能够调用静态域,也能够调用静态方法,可是不提倡,见下条。

※,能够使用对象调用静态方法。例如,若是harry是一个Employee对象,能够用harry.getNextId()代替Employee.getNextId()。不过,这种方式很容易形成混淆,其缘由是getNextld方法计算的结果与harry毫无关系。咱们建议使用类名,而不是对象来调用静态方法

※,在下面两种状况使用静态方法:

  • 一个方法不须要访问对象状态(访问对象状态意思即 实例/对象 做为方法的调用者,实例/对象 也称为隐式参数),其所需参数都是经过显式参数提供(例如:Math.pow)。相反的例子是:实例化一个日期对象LocalDate date, date.plusDays(100),这个方法依赖于对象的状态(某个日期)。
  • 一个方法只须要访问类的静态域。

※,若是查看一下System类,就会发现有一个setOut方法,它能够将System.out设置为不一样的流。读者可能会感到奇怪,为何这个方法能够修改final变量的值。缘由在于,setOut方法是一个本地方法,而不是用Java语言实现的。本地方法能够绕过Java语言的存取控制机制。这是一种特殊的方法,在本身编写程序时,不该该这样处理。

System.setOut(new PrintStream(new File("xxxx\\a.txt")));
System.out.println("hello out");//往文件中打印了hello out

※,术语“static”有一段不寻常的历史。起初,C引入关键字static是为了表示退出一个块后依然存在的局部变量在这种状况下,术语“static”是有意义的:变量一直存在,当再次进入该块时仍然存在。随后,static在C中有了第二种含义,表示不能被其余文件访问的全局变量和函数。为了不引入一个新的关键字,关键字static被重用了。最后,C++第三次重用了这个关键字,与前面赋予的含义彻底不同,这里将其解释为:属于类且不属于类对象的变量和函数。这个含义与Java相同。

※,工厂方法:

20,方法参数:按值调用 和 按引用调用。

※,Java 程序设计语言老是采用按值调用。有些程序员(甚至本书的做者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的(P137)。Java方法能够改变对象参数的状态,但这种改变的原理并非引用传递,而是形参获得的是对象引用(即实参是对象的引用)的拷贝,对象引用及它的拷贝同时引用同一个对象。具体参考书中叙述图解。

※,(C语言资料,这段话对理解Java对对象的按值调用颇有帮助!)形参至关因而实参的“别名”,对形参的操做其实就是对实参的操做,在引用传递过程当中,被调函数的形式参数虽然也做为局部变量在栈中开辟了内存空间,可是这时存放的是由主调函数(本身理解:main函数)放进来的实参变量的地址。被调函数对形参的任何操做都被处理成间接寻址,即经过栈中存放的地址访问主调函数中的实参变量。正由于如此,被调函数对形参作的任何操做都影响了主调函数中的实参变量。

※,下面总结一下Java中方法参数的使用状况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法能够改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

21,对象构造器

※,方法名和方法参数类型 在一块儿叫作方法签名。方法返回类型不是方法签名的一部分。方法重载(英文名实际上叫超载,是类的能力,超载的能力)须要方法的签名不一样。

※,域(即类的属性)与局部变量的主要不一样点:必须明确地初始化方法中的局部变量。可是,若是没有初始化类中的域,将会被自动初始化为默认值(数值为0、布尔值为false、对象引用为null,如String类型默认为null)。

※,若是类中没有任何一个构造器,那么系统会提供一个无参数构造器,这个构造器将全部的实例域设置为默认值。因而,实例域中的数值型数据设置为0、布尔型数据设置为false、全部对象变量将设置为null。注意只有类中没有任何构造器时系统才会提供一个默认的无参数构造器,若是类中至少有一个构造器,可是没有提供无参数构造器,则在构造对象时没有提供参数会被视为不合法。

※,显式域初始化:能够经过不一样重载的构造器设置类的实例域的初始状态。当一个类的全部构造器都但愿把相同的值赋予某个特定的实例域时,能够直接在类声明中将初始值赋给域。

  域的初始值不必定是常量,例如:

class Employee {
    private static int nextId = 1; private int id = assignId();// 初始化对象时执行 Employee e = new Employee() private String name; Employee (String name) { this.name = name; } public static int assignId() { int r = nextId; nextId++; return r; } }

※,调用另外一个构造器:类中的this指代类方法的隐式参数,java类中,this能够省略,但最好带上。this关键字还有另一个含义,即调用另外一个构造器。例如,

publicEmployee(doubles)
{
    //calls Employee(String, double)
    this("Employee#" + nextld, s);// 形如这样,表示调用另外一个构造器
    nextld++; }

※,初始化块 ☆

1, 前面讲了java两种初始化数据域的方法:①在构造器中设置值。②在声明中赋值。实际上java还支持第三种机制:初始化块。初始化块中能够有多行代码,只要构造类的对象,这些块就会被执行。

2,初始化数据域的顺序:

  1. 全部数据域被初始化为默认值(0、false或null)。
  2. 按照在类声明中出现的次序,依次执行全部域初始化语句和初始化块。
  3. 执行构造器方法。

3,若是静态域的初始化代码比较复杂也能够使用静态初始化块。只要在代码放在一个块中,并标记关键词static便可。在类第一次加载时,全部的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。

如将静态域nextId起始值赋予一个10000之内的随机整数:

    static {
        Random generator = new Random(); nextId = generator.nextInt(10000); }

4,

※,对象析构与finalize方法: 因为Java有自动的垃圾回收器,不须要人工回收内存,因此Java不支持析构器。

22,包

※,java.lang包是被默认导入的。

※,全部标准的java包都处于java 和 javax 包层次中。

※,从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系。每个都拥有独立的类集合

※,修饰符:public,package-private(即没有任何修饰符时的默认值),protected,private

  • 类的权限修饰符有两个:public(对任何地方的类都是可见的),package-private(只对本身所在的包内的全部类可见,注意嵌套的包之间毫无关系)。
  • 类中成员的修饰符有4个:public, package-private(只对本身包内的全部类可见), protected( 对本身包内的全部类以及其余包内本类的子类可见),private(只对本类可见)
  • 注意 protected: 若子类与父类不在同一包中,那么在子类中,子类实例能够访问其从父类继承而来的protected方法,而不能访问父类实例的protected方法,一个典型的例子就是Object类中的clone()方法,虽然是protected修饰符,可是不在java.lang包中的子类若是不重写这个clone()方法是没法直接调用Object的clone()方法的。参见此篇文章
  • Java中的protected概念和C++稍有不一样, 比C++中的 protected 安全性差。
修饰词 本类 同一个包的类 继承类 其余类
private × × ×
无(默认) × ×
protected ×
public

※,静态导入:import语句不只能够导入类,还增长了导入静态方法和静态域的功能。

  import static java.lang.System.*;    out.println("fuck U");//

※,若是没有在源文件中放置package语句,这个源文件中的类就被放置在一个默认包(default package中),默认包是一个没有名字的包。类的路径必须和包名一致。

  javac ./com/learn/java/Test.java   //编译器命令能够在任何目录下执行,只要能找到源代码文件(编译后的class文件叫类文件,java为后缀名的文件叫源文件)便可。

  java com.learn.java.Test  // 解释器命令必须在基目录下执行,即包含子目录com的目录


※,若是没有指定public或private, 这个部分(类、方法或变量(域))能够被同一个包中的全部方法访问。

※,类路径(class path): 类路径就是全部包含类文件(即编译后的class文件)的路径的集合,即告诉编译器和虚拟机去哪儿找类文件。

  • 在Unix系统中,不一样的类路径之间用冒号分隔,
  • Windows环境中,用分号分隔。以下类路径:
    • c:\classdir;.;c:\archives\archive.jar 包含3个部分,第二个是当前路径,第三个是jar包,jar包是包含一系列class文件压缩包。java虚拟机寻找类的时候能够在jar包里搜索class文件。
  • 因为运行时库文件(jre/lib/rt.jar和在jre/lib 与 jre/lib/ext 目录下的一些其余的jar文件)会被自动地搜索,因此没必要将它们显式地列在类路径中。
  • java 虚拟机搜寻类文件过程。
  • 编译器搜寻定位源代码文件的过程。
  • 设置类路径:
    • 采用-classpath(或 -cp)选项指定类路径,这是设置类路径的首选方法: java -classpath 'c:\classdir;.;c:\archives\archive.jar' MyProg。整个指令必须书写在一行,经测试Windows下类路径要用引号引发来。
    • 除首选方法外,也能够经过设置CLASSPATH环境变量来设置类路径,直到退出shell为止,类路径设置均有效。
    • 在bash中,命令以下,export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
    • 在Windows shell中,命令以下, set CLASSPATH=c:\classdir;.;c:\archives\archive.jar。
    • 有人建议将CLASSPATH环境变量设置为永久不变的值。总的来讲这是一个很糟糕的主意。
    • 有人建议绕开类路径,将全部的文件放在jre/lib/ext路径。这是一个极坏的主意。

※,JAR(Java Archive)文件:

  • JAR文件能够将类文件(即class文件)打包(使用zip压缩方式)成一个单个的文件,里面能够包含类文件(即class文件),也能够包含图片或音频等文件。
  • 能够使用jar命令建立jar文件,jar.exe是JDK默认安装的一部分,位于jdk/bin目录下。使用方法和Linux下的tar命令很相似。
  • jar -cvf jarFileName.jar(此处能够为绝对路径或相对路径,如:D:/test.jar) file1 file2 file3 ... //file 能够是任意的文件, 通常主要是class文件和资源文件
  • 参数解释:
    • -c 建立一个新的jar文件,并将指定的文件添加其中。若是指定的文件是文件夹,jar程序将自动递归处理。
    • -C 改变目录。jar cvf jarFileName.jar -C ../  xx.class  //将当前目录的上一级目录中的xx.class文件添加到jar文件中。
    • -e 建立一个manifest条目
  • MANIFEST.MF文件
    • 每一个jar文件里都有一个 指示 文件MANIFEST.MF。位于jar文件的META-INF子目录下。
    • MANIFEST.MF文件包含若干个节(section),第一节是主节,描述的是整个jar文件。不一样的节之间使用空行分隔。主节以外的其余节能够描述单个的文件或包或URL等。这些副节中的条目必须有一个Name条目打头。例如:
      Manifest-Version: 1.0
      
      Name: Woozle.class Name: com/mycompany/mypkg/ 
  • 执行jar文件:
    • 使用命令 jar -cvfe  jarFileName.jar com.learn.java.Test  xx.class yy.class 能够在MANIFEST.MF的主节中添加一个条目: Main-Class: com.learn.java.Test   或者手动在文件里添加也行。
    • 而后就能够使用 java -jar jarFileName.jar 运行这个jar文件,从刚才设置的主类开始运行。
    • Java 9以后支持多版本的jar包。在jar包里能够设置多个版本的类文件。具体待研究

※, 文档注释:JDK中包含一个颇有用的工具叫javadoc。Java的API文档就是经过对标准Java类库的源代码运行javadoc生成的。

  • 注释中如要添加等宽字体,不要使用<code>xxx</code> 而要使用{@code something} ,就不用担忧<字符的转义了。
  • 包注释方法:
  • 运行javadoc命令生成注释文档的方法:
    • 切换到想要生成文档的源文件目录,若是有嵌套的包须要生成文档,例如com.learn.java,就必须切换到基目录,即包含子目录com的目录。
    • javadoc - d docDirectory nameOfPackage // 一个包
    • javadoc - d docDirectory nameOfPackage1 nameOfPackage2  . . .// 多个包
    • javadoc -d docDirectory *.java // 默认包的文档生成

※,类设计技巧

  • 必定要保持数据的私有(实例域的私有性),不要破坏封装性。
  • 必定要对数据初始化。Java不对局部变量进行初始化(若是局部变量没有赋初值就使用,编译器会报错:变量没有初始化),可是会对对象的实例域进行初始化,可是最好不要依赖于系统的默认值,而是应该显式地初始化全部的数据。
  • 不要在类中使用过多的基础数据域,最好将有关联的数据域封装在一个类中,而后引用这个类。好比:使用一个Address类封装如下字段,而后在Customer类中引入Address类,而不是直接在Customer类中使用这些基础数据域。

    private String street;
    private String city;
    private String state;

  • 不是全部的数据域都须要独立的域访问器和域更改器。在构造类的对象后,经常有一些不但愿别人获取或设置的实例域,这些域就不须要设置任何访问器或更改器。
  • 将职责过多的类进行分解。这个须要经验积累。书中有个例子: 一副牌 和 一张牌 各设计为一个类,而不是将两个概念混在一个类中。
  • 类名和方法名要可以体现它们的职责。
  • 优先使用不可变的类(immutable classes)。LocalDate类以及java.time包中的其余类是不可变的—没有方法能修改对象的状态。相似plusDays的方法并非更改对象,而是返回状态已修改的新对象。更改对象的问题在于,若是多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。若是类是不可变的,就能够安全地在多个线程间共享其对象。所以,要尽量让类是不可变的,固然,并非全部类都应当是不可变的。若是员工加薪时让raiseSalary方法返回一个新的Employee对象,这会很奇怪。

※,

※,

第五章,继承(inheritance)

※,本章还阐述了反射reflection)的概念。反射是指在程序运行期间发现更多的类及其属性的能力。这是一个功能强大的特性,使用起来也比较复杂。因为主要是开发软件工具的人员,而不是编写应用程序          的人员对这项功能感兴趣,所以对于这部份内容,能够先浏览一下,待往后再返回来学习。

※,关键字extends代表正在构造的新类派生于一个已存在的类。已存在的类称为超类(super class)、基类(base class)或父类(parent class);新类称为子类(sub class)、派生类(derived class)或孩子类(child class)。

※,Java是单一继承,即只容许继承一个类。

※,Java子类继承父类时,并无继承父类的私有方法可是若是父类的公有方法或protected方法访问了父类的私有属性,那么子类对象也能够访问到父类的这些私有属性。官方文档说明以下:A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.点我Java官方文档】。

※,方法重写(Override) 【点我查看官方文档

  • 子类继承父类时,若是子类和父类方法签名相同(不考虑父类的private方法),那么返回值类型也要兼容。不容许子类方法的签名和父类相同,可是返回值类型不一样。缘由是若是容许这样,子类就会继承父类的那个方法,那么子类方法中就有两个方法签名相同可是返回值类型不一样的方法。这就至关于重载时只有返回值类型不一样的两个方法,是不被容许的。
  • 注意:父类中的private 方法没法被子类继承,所以就不存在方法重写。就是说,父类有一个private方法,子类能够有一个同签名可是返回值类型不一样的方法。
    class Super {
        private void get() {}
    }
    class Sub extends Super {
        /**
         * 子类没有继承父类的私有get()方法
         * 所以能够签名相同可是返回值类型不一样
         */
        public String get() {
            return "到头来都是为他人做嫁衣裳";
        }
    }
  • 方法重写含义: 若是一个子类方法的 签名(名称和 参数个数、类型) 和 返回值 和父类的一个方法的签名和返回值相同(子类返回值也能够是父类返回值的子类型),那么子类中的这个方法重写了父类中的这个方法。只要知足条件不管加不加@Override注解都是方法重写。注意:重载(overload)要求是:方法名称相同,可是参数个数或类型不一样,重载并不检查返回类型。
  • final 方法没法被子类重写。可是子类能够重载一个同名方法。final类没法被继承。
  • static 方法没法被子类实例方法重写。可是子类能够重载一个同名方法。
  • 父类的static方法,子类也能够有一个方法签名和返回值类型相同(兼容)的static 方法。此时叫作子类的静态方法隐藏了父类的同名静态方法。
  • 子类静态方法没法隐藏父类同签名同返回值类型的实例方法(编译错误)。
  • 父类有一个static方法,子类也有一个同签名的static方法,此时亦要求两个方法的返回值类型兼容。
  • 总结一下(不保证100%正确):
    • 只要子类和父类的方法签名相同(父类方法是private时,不在此规则内),那么两个方法的返回值类型也要兼容。
  •  

※,子类中使用super关键词调用超类的方法。有些人认为super和this引用是相似的概念,这是错误的。super不是一个对象的引用,不能将super赋给另外一个对象变量(而this是对象引用,能够赋值给另外一个变量)。super 只是一个指示编译器调用超类方法的的特殊关键字

※,子类构造器:

  • 能够使用super(String name, int id) 实现对超类一样函数签名的构造器的调用。使用super调用超类构造器的语句必须是子类构造器的第一条语句。
  • 若是子类构造器没有显式调用超类的构造器,则编译器会自动调用超类默认(没有参数)的构造器。若是超类中没有不带参数的构造器,而且在子类的构造器中没有显式的调用超类的其余构造器,那么Java编译器将报错!

※,多态(polymorphism):此书中多态是在继承章节中的一小节,多态是继承致使的,继承是多态的前提。

0,两个简单例子:

1, 
Parent p = new Child(); // 当用父类引用来接收一个子类类型的对象时,对象变量p被编译器视为是Parent类型的,可是调用父子类中都有的方法p.getName()时(注意此时若是Parent类中若是没有getName()方法,编译器会报错)
,实际调用的是子类Child中的getName方法。 p实际指向的是子类对象的引用。 2, Parent[] parents = new Parent[3] Child c = new Child(); parents[0] = c; parent[1] = new Parent(); parent[2] = new Parent(); for (Parent e : parents) { System.out.println(e.getName()); } // 尽管这里将e声明为Parent类型,但实际上e既能够引用Parent类型的对象,也能够引用Child类型的对象。 // 当e引用Parent对象时,e.getName()调用的是Parent类中的getName方法,当e引用Child对象时,e.getName()调用的是Child对象的方法。
像这种在运行时可以自动地选择调用哪一个方法的现象称为动态绑定(dynamic binding)

1,一个对象变量能够指示多种实际类型的现象叫作多态。

2,多态存在的三个条件:

  • 继承
  • 重写(方法覆盖)
  • 父类引用指向子类对象

3,当使用多态方式调用方法时,首先检查父类中是否含有该方法,若是没有则编译器报错;若是有再去调用子类的同签名方法(具体叙述见下)。

3.1, 注意:父类引用指向子类的对象时,父类引用只能调用父类已有的方法。若是子类没有重写父类的方法,就不存在多态,由于调用的仍是父类的(实际上,根据下面动态绑定的论述,本身推断以下:父类引用在动态绑定阶段,查看的是子类对象的方法表,没有重写的状况下就使用子类继承来的同签名方法。)。所谓的多态就是须要对同一个方法的调用产生不一样的状态,不重写也就没有多态(但也不会报错)。

4,静态绑定和动态绑定:(Java编译器将源码编译成class文件供Java虚拟机执行)

  • 静态绑定(前期绑定)是指在程序运行前就已经知道方法是属于哪一个类的,在编译时就能够链接到类中,定位到这个方法。在Java中,final,static, private修饰的方法以及构造函数都是静态绑定的,不需程序运行,不需具体的实例对象就能够知道这个方法的具体内容。
  • 动态绑定(后期绑定)是指在程序运行过程当中根据具体的实例对象才能具体肯定是哪一个方法。动态绑定是多态得以实现的重要因素。动态绑定经过方法表来实现:虚拟机预先为每一个类建立一个方法表(method table),在真正调用方法的时候虚拟机仅查找这个表就好了。方法表中记录了这个类中定义的方法的指针,每一个表项指向一个具体的方法代码。若是这个类重写了父类中的某个方法,则对应的表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面。

5,向上转型(upcasting) 和 向下转型(downcasting)

  • 向上转型:通俗的讲向上转型就是将子类对象转为父类对象,此处父类对象能够为接口。向上转型不须要强制转换。

  • 向下转型:将父类对象转为子类对象叫作向下转型。向下转型须要强制转换。且有可能出现编译经过但运行时错误的向下转型。
  • Parent p = new Child(); // 这个就叫作向上转型。无需强制转换。
    Child c = (Child) p;// 这个就叫作向下转型。须要强制转换。此时编译和运行都不会报错,由于p实际指向是一个子类对象。
    System.out.println(p instanceof Child);// true System.out.println(p instanceof Parent);// true Parent p1 = new Parent(); Child c1 = (Child) p1;// 这里编译器不会报错,可是运行时会报错(ClassCastException),由于p1实际指向的是父类的对象。 System.out.println(p1 instanceof Child);// false System.out.println(p1 instanceof Parent);// true

    应该养成这样一个良好的程序设计习惯:在将超类转换成子类以前,应该使用 instanceof 进行检查是否可以转换成功。
    null instanceof C ;// 始终为false。由于null没有引用任何对象,固然也不会是引用C类型的对象。
  • 向上转型的一个好处就是能够使代码变得简洁。好比:
    Animal类有3个子类:Cat, Dog, Bird。每一个子类都重写了Animal类的 bark()方法,每种动物的叫声都不同。
    现有一个Test类,其中有个方法是getBark(Animal animal)。此时参数只要是父类的Animal便可,
    Test的实例调用getBark()方法时能够传入不一样的动物,如getBark(cat)等等,此方法能够根据传入的不一样的动物类型发出正确的叫声。
    若是没有向上转型,那么getBark()这个方法就须要写多个,有几个子类动物就须要写几个。

6,动态绑定的编译、运行原理:

  • 编译阶段:向上转型时是用父类引用执行子类对象,并能够用父类引用调用子类中重写了的同签名方法。可是不能调用子类中有但父类中没有的方法。缘由在于在代码的编译阶段,编译器经过声明的对象的类型(即引用自己的类型)在方法区该类型的方法表中查找匹配的方法(最佳匹配法:参数类型最接近的被调用,好比int能够转成double),若是查找到了则编译经过。向上转型时,父类引用的类型是父类,因此编译器在父类的方法表中查找匹配的方法,因此子类中新增的方法是查不到的,若是查不到编译器就会报错。
  • 运行阶段:当Parent  p = new Child(); p.say();语句编译经过后,进入Java虚拟机执行阶段,执行Parent  p = new Child()语句时,建立了一个Child实例对象,而后在p.say()调用方法时,JVM会把刚才建立的Child对象压入操做数栈,用它来进行调用,这个过程就是动态绑定:即用实例对象所属的类型去查找它的方法表,找到匹配的方法进行调用。子类的方法表包含从父类继承来的方法以及本身新增的方法,若是p.say()在子类中被重写了,那么JVM就会调用子类中的这个方法, 若是没有被重写,那么JVM就会调用父类的这个方法。以此类推。

7,子类覆盖父类的方法(即方法重写)时,子类方法不能下降父类方法的可见性。特别是,当父类方法是public时,子类方法必定要是public。

※,阻止继承:final类和方法

  • 能够将类中的某个方法声明为final,表示这个类的这个方法不能被子类重写。
  • 也能够将类声明为final,表示这个类不能被继承。被声明为 final 的类的全部方法会自动的成为 final 方法。注意,只是final 类的方法会自动成为 final 方法,final类中的域不会自动成为final域。

※,强制类型转换:

※,抽象类:

  •  能够用关键字 abstract 将一个方法定义为一个抽象方法,抽象方法只有签名和返回类型,不须要实现。
  • 类中只要含有抽象方法,那么这个类就必须被定义为抽象类。
  • 抽象类中没必要全是抽象方法,也能够含有具体实现的方法。
  • 继承抽象类时能够:①不实现抽象方法,那么这个子类也必须被定义为抽象类;②实现所有抽象方法,那么这个子类就能够没必要为抽象类。
  • 一个类中即便不含有任何抽象方法, 也能够将类声明为抽象类。
  • 抽象类不能被实例化,可是能够定义一个抽象类的对象变量,这个对象变量只能引用非抽象子类的对象。例子以下:
  • 假设,Person是抽象类,Student和Employee是Person的非抽象子类。
    1,Person p = new Student("晴雯"); 2, Person[] people = new Person[2]; people[0] = new Student("黛玉"); people[1] = new Employee("秦可卿");

※※※,Object 类: 全部类的超类

一,概述

  • Object 类是Java中全部类的超类,在Java中每一个类都是由它扩展而来的。因此熟悉这个类提供的全部服务十分重要。本章介绍一些基本的内容,没有提到的部分能够参考后面的章节或在线文档
  • 在Java中,只有基本类型(8种)不是对象。
  • Java中全部的数组类型,无论是对象数组仍是基本类型数组都继承了Object类。

二,equals()方法:

※,Object 类中的 equals()方法仅在两个对象具备相同的引用(即两个对象指向同一块存储区域)时才返回true,这是Object类的默认操做。可是对于多数类来说,这种判断并无什么意义。实际上,常常须要检测两个对象状态的相等性,若是两个对象状态相等(即某些域相等)就认为这两个对象是相等的。因此常常须要重写这个方法,以实现对象状态相等(即某些域相等)就能够返回true。

※,假设两个  Employee  对象,若是对象的姓名,薪水,和雇用日期都是相同的,就认为他们是相等的。如下为Employee类重写的equals()方法:

public boolean equals(Object otherObject) {
        // 两个对象引用是否指向同一个对象,即彻底相同
        if (this == otherObject) return true; if (null == otherObject) return false; // this.getClass(): 获取类的名称。这里用getClass()判断实际有点争议,见下文 if (getClass() != otherObject.getClass()) return false; // 如今otherObject确定是个非null的 Employee对象 Employee other = (Employee) otherObject; /* * 为防备name 或 hireDay可能为null的状况,这里使用了Objects.equals()方法而不是直接用name.equals(other.name). */ return Objects.equals(name, other.name)&& salary == other.salary && Objects.equals(hireDay, other.hireDay); }

※,在定义子类的equals()方法时,首先要调用超类的equals()方法,若是检测失败,对象就不能相等。若是超类中的域到相等,就须要比较子类中的实例域。

  如下为Employee的子类Manager类重写的equal()方法

public boolean equals(Object otherObject) {
        if (!super.equals(otherObject)) return false; // 经过了超类的检测,说明otherObject 和 this 属于同一个类:Manager Manager other = (Manager) otherObject; return bonus == other.bonus; }

※,继承中涉及到的equals()方法:

1,上面重写的equals()方法使用了对象的getClass()方法比较两个对象是否属于同一个类来做为是否equals的一个条件,这个条件有些争议。以下论述:

  • 若是如今有需求:一个Manager对象的Id和一个Employee对象的Id相等,就认为这个Manager对象和这个Employee对象是相等的,那么此时getClass()方法便再也不适用了。此时Employee类中的equals方法须要使用 instanceof 来进行检测: if (!(otherObject instanceof Employee)) return false;

2,反过来说,若是使用 instanceof 做为判断两个对象equals的条件,也有不合适的地方。以下论述:

  Java语言规范要求equals()方法必须具备以下特性:

  • 自反性(reflexive): 对于任何非空引用x, x.equals(x)应该返回true。
  • 对称性(symmetric): 对于任何引用x和y, 当且仅当x.equasl(y)返回true,y.equals(x)也应该返回true。
  • 传递性(transitive): 对于任何非空引用x,y和z,若是x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
  • 一致性(consistent):若是x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回一样的结果。
  • 对于任意非空引用x, x.equals(null)应该返回false。

  假设e为Employee的一个对象,m为Manager的一个对象,若是使用instanceof做为判断两个对象equals的条件,根据上面的对称性规则,若是e.equasl(m)为true,那么m.equals(e)也必须返回true。这就使得Manager类受到了束缚:这个类的equals方法必须可以用本身与任何一个Employee对象进行比较,这样就会忽略Manager对象特有的信息。这样就会致使两个Manager对象没法比较是不是equals的, 只要m1和 m2 的name, salary, hireDay一致,那么两个对象就是equals的, 没法比较两个Manager对象的bonus是否相等。解决方法见下。

3,关因而使用getClass()方法仍是使用 instanceof 操做符来做为判断两个对象是否equals的条件,能够从如下两个角度看待:

  • 若是子类可以拥有本身的相等概念(好比,须要bonus也相等,两个manager对象才相等),则因为equals()方法的对称性,须要使用getClass()方法来判断是否相等。
  • 若是由超类决定相等的概念(好比,只要Employee或Manager的两个对象的id相等,这两个对象就是相等的),那么就能够使用instanceof进行检测。这样能够在不一样子类的对象之间进行相等的比较。

※,关于equals()方法的一种常见的错误:

public boolean equals(Employee otherObject) {
        // 两个对象引用是否指向同一个对象,即彻底相同
        if (this == otherObject) return true; if (null == otherObject) return false; // this.getClass(): 获取类的名称 if (getClass() != otherObject.getClass()) return false; // 如今otherObject确定是个非null的 Employee对象 Employee other = (Employee) otherObject; /* * 为防备name 或 hireDay可能为null的状况,这里应该使用Objects.equals()方法优化一下 * Objects.equals(name, other.name); Objects.equals(hireDay, other.hireDay); */ return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay); }
// 错误的地方在于equals()方法的参数类型是Employee。其结果是这个equals()方法并无覆盖Object类的equals()方法,而是定义了一个彻底无关的方法。为了不发生类型错误,
能够使用@Override(比...更重要)对覆盖超类的方法进行标记。若是出现了错误而且正在定义一个新的方法,编译器就会给出错误报告。

※,java.util.Objects.equals(a, b)方法: 这个方法对null是安全的, 若是两个参数都为null,Objects.equals(a,b)调用将返回true;若是其中一个参数为null,则返回false;不然,

  若是两个参数都不为null,则调用a.equals(b)

※,java.util.Arrays.equals(arr1, arr2): 对于两个数组类型,能够使用Arrays.equals()检测两个数组是否相等。若是两个数组以相同的顺序包含相同的元素,则他们是相等的,不然就是不相等的。

 三,hashCode()方法:

※,int java.lang.Object.hashCode(): 返回对象的散列码。散列码能够是任意的整数,能够整数也能够负数。两个equals相等的对象要求返回相等的散列码。

※,散列码(hash code)是由对象导出的一个整形值。散列码是没有规律的,若是x和y是两个不一样的对象,那么x.hashCode()和y.hashCode()基本不会同样。

※,hashCode()方法定义在Object类中,所以每一个对象都有一个默认的散列码,这个散列码值是由对象的内存存储地址推导出来的。

※,若是从新定义equals()方法,就必须从新定义hashCode()方法,以便用户能够将对象插入到散列表中(散列表后面讲述).

※,hashCode()方法的定义:

  • hashCode()方法应该返回一个整形数值(能够是负数)。合理的组合实例域的散列码以便可以让各个不一样的对象产生的散列码更加均匀。下面是Employee类的hashCode()方法的一个例子:
    @Override
    public int hashCode() { /** * static int java.util.Objects.hashCode()方法是null安全的方法,若是参数为null,返回0. * 不然对参数调用hashCode()方法。好比name.hashCode(); hireDay.hashCode(); * 使用静态方法 static int java.lang.Double.hashCode()能够避免创造Double对象:new Double(salary).hashCode(); */ return 7 * Objects.hashCode(name) + 11 * Double.hashCode(salary) + 13 * Objects.hashCode(hireDay); }
  • 定义Employee类的hashCode()方法还有一个更好的方法就是使用Objects.hash()并提供多个参数。这个方法会对各个参数调用Objects.hashCode()并组合这些散列值。即:
  • @Override
        public int hashCode() {
       // static int java.util.Objects.hash(Object... objects) return Objects.hash(name, salary, hireDay); }
  • static int java.util.Arrays.hashCode(Type[] a): 计算数组a的散列码。这个散列码有数组元素的散列码组成。

※,equals()方法与hashCode()方法的定义必须一致:若是x.equals(y)方法返回true,那么x.hashCode()就必须和y.hashCode()具备相同的值。不然就会出现问题(具体什么问题待研究)。也就是说,equals相等的两个对象的hashCode也要保证相等。可是反过来两个hashCode相等的对象不必定须要equals相等(这个好理解:假设两个不一样的类有相同的属性和hashCode规则,那么hashCode相等而不equals相等)。好比,若是用定义的Employee.equals()比较雇员的ID,那么hashCode()方法就须要散列ID而不是雇员的姓名或继承自Object的存储地址推导出来。

四:toString()方法:

※,Object类中还有一个很重要的方法toString(),它用于返回表示对象值的字符串。Object类默认的toString()方法返回值是:对象所属的类名和散列码。以下例

  • 首先:1,调用println(x)方法会直接调用x.toString()方法。2,只要一个对象与一个字符串经过操做符"+"链接起来,Java编译器就会自动调用toString()方法,以便得到这个对象的字符串描述。
  • 例如调用System.out.println(System.out); 输出如下内容:java.io.PrintStream@15db9742。由于PrintStream类的设计者没有重写覆盖Object类的toString()方法,直接继承了Object类的toString()方法。
  • Java中的数组也没有实现本身的toString()方法,也是继承了Object类的toString方法。因此int[] arr = {1,2,3,4}; System.out.println(arr);//输出结果是【 [I@6d06d69c】,【前缀[I代表是一个整形数组】。修正的方式是调用静态方法 Arrays.toString(arr) ,打印多维数组就使用Arrays.deepToString(arr)方法。

※,绝大多数重写了toString()方法的类都遵循这样的规则:类的名字,随后是一对方括号括起来的域值。

  • 好比,Point p = new Point(10,29); System.out.println(p);// 输出结果是: java.awt.Point[x=10,y=29]
  • 下面是Employee类中toString()方法的实现
        /**
         * 不直接写Employee而是使用this.getClass().getName()获取类名更具普适性。
         * 好比继承了此类的子类实现本身的toString()方法时能够直接调用super.toString()方法就能够获得子类本身的类名
         */
        @Override
        public String toString() { return this.getClass().getName() + "[name=" + this.name + ",salary=" + this.salary +",hireDay=" + this.hireDay +"]"; }
  • 子类也应该有本身的toString()方法,如下是Manager类的toString()方法的实现:
        @Override
        public String toString() { return super.toString() +"[bonus=" + this.bonus +"]"; }

※,在调用x.toString()方法的地方能够使用【""+x】替代,此时编译器会自动调用x.toString()。这种写法的好处是:若是x是基本类型,基本类型却没有toString()方法,这条语句照样能够执行。

※,API

  • java.lang.Object  Class getClass();//返回包含对象信息的类对象
  • java.lang.Class String getName();//返回类名
  • java.lang.Class Class getSuperclass();//以Class对象的形式返回这个类的超类信息。

24,泛型数组列表(Generic Array List)

※,出现数组列表ArrayList的背景

  • 在C++中,必须在编译的时候就要肯定整个数组的大小,这个很不方便。好比,有的员工部门100个员工,有的只有10个,愿意为仅有10个员工的部门浪费90个员工占据的存储空间吗?
  • Java中,状况好一些。它容许在运行时肯定数组的大小: int actualSize = ...(动态肯定大小的代码); Employee[] staff = new Employee[actualSize]; 固然这段代码并无彻底解决运行时动态更改数组的问题。一旦肯定了数组的大小,它就不可更改了。
  • Java中解决这个问题最简单的方法是使用Java中另一个被称为ArrayList的类。

※,使用ArrayList类: java.util.ArrayList<E>  SE1.2

  • ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,须要用一对尖括号将类名括起来加在后面,如ArrayList<Employee>。
  • ArrayList<Employee> staff = new ArrayList<Employee>();// 在Java SE7以后,能够省去右边的类型参数。即ArrayList<Employee> staff = new ArrayList<>();编译器将检查变量staff的泛型类型,而后将这个类型放入右边的<>中
  • 使用add()方法添加一个元素到数组列表中。staff.add(new Employee());

※,动态改变大小的原理

  • 数组列表管理着对象引用(staff)的一个内部数组。若是调用add()方法时内部数组空间已经被用完了,数组列表就将自动建立一个更大的数组,并将全部的对象从较小的数组中拷贝到较大的数组中。
  • 若是可以估计出数组可能存储的元素数量,能够在填充数组以前就肯定数组列表的容量,有以下两个方法:
    • 调用ensureCapacity()方法,staff.ensureCapacity(100);
    • 将初始容量传递给ArrayList构造器。ArrayList<Employee> staff = new ArrayList<>(100);
    • 指定容量后,编译器将分配一个包含100个对象的内部数组。而后调用100次add()方法而不用从新分配空间。
  • 分配数组列表,以下所示
    new ArrayList<Employee>(100) // capacity is 100,but size now is 0.
    它与为新数组分配空间有所不一样: new Employee[100] // size is 100 数组列表的容量与数组的大小有一个很是重要的区别。若是为数组分配100个元素的存储空间,数组就有100个空位置能够使用。而容量为100个元素的数组列表只是 拥有保存100个元素的潜力(实际上从新分配空间的话,将会超过100),可是在最初,甚至是完成初始化构造以后,数组列表根本就不含有任何元素。
    ---------------------------------------------------------------------------------------------------------------------------
    Employee[] staff = new Employee[2];
    System.out.println(Arrays.toString(staff));// 打印:[null, null]
    System.out.println(staff.length);// 打印:2
    ----------
    ArrayList<Employee> staff = new ArrayList<>(2);
    System.out.println(staff);// 打印:[]
    System.out.println(staff.size());//打印:0
  • 一旦可以确认数组列表的大小再也不发生变化,就能够调用trimToSize()方法。这个方法将存储区域的大小调整为当前元素数量所须要的存储空间数目。垃圾回收器将回收多余的存储空间。注意一旦调用了trimToSize()方法,再添加新元素就须要花时间再次移动存储块。因此应该在肯定不会再添加任何元素时再调用trimToSize()方法。

※,访问ArrayList元素

  • ArrayList类并非Java程序设计语言的一部分,它只是一个由某些人编写且被放在标准库中的一个实用类(能够理解为ArrayList是Array的一个增强版)。访问ArrayList的元素使用的语法是get()和set()方法,而不是Java中的[]语法格式。
  • list.set(i, xxx);//用于设置数组列表list的第i个元素,将其设置为xxx。注意,这个方法只能替换数组中已经存在的元素内容,若是不存在,代码运行时会报错。
    ArrayList<Employee> employees = new ArrayList<>(100);//容量100,可是大小此时仍是0
    employees.set(0, new Employee());// employees中还没有含有第0个元素,编译经过但运行时报错。
  • 使用add()方法添加新元素而不要用set()方法。
  • 能够使用toArray()方法将ArrayList类型转换为Array类型
           ArrayList<Employee> list = new ArrayList<>();
            for (int i = 0; i < max; i++) { x = ... list.add(x) } Employee[] staff = new Employee[list.size()]; list.toArray(staff);//list中的元素从前日后依次添加到staff中。 /** * list.toArray()方法详解: * 1,此方法始终有返回值:Object[],即将list中的元素从前日后依次添加至返回值arr中 * 2,若是数组参数staff的大小与list的大小相同,那么除了添加至返回值arr中以外,list中的元素也会依次复制到staff中 * 3,若是staff的大小大于list的大小,arr和staff同样,除了list中的元素外,多余的元素用null填充 * 4,若是staff的大小小于list的大小,那么staff将保持不变,不会被填充。arr则会正常填充list中的全部元素。这种状况和 * 直接调用不带参数的toArray()效果相同。 * Object[] orr = list.toArray();效果等同于Object[] orr2 = list.toArray(new X[0]) */ Object[] arr = list.toArray(type[] 
  • 1

※,插入或删除ArrayList元素

  • 使用带索引的add()方法插入元素:java.util.ArrayList void add(int index, E obj); // 在index位置插入一个元素,index以后的全部元素后移一个位置,并将数组大小加1
  • java.util.ArrayList E remove(int index);// 删除一个元素,后面的元素前移一位。被删除的元素由返回值返回。index只能是0~size-1之间。
  • 对数组实施插入或删除元素的操做效率比较低。对于小型数组没必要担忧,可是若是数组存储的元素比较多,有常常须要在中间位置插入、删除元素,就应该考虑使用链表了(后面讲链表)。

25,对象包装器与自动装箱(Object Wrappers and AutoBoxing)

※,有时须要将基本类型转换为对象。全部的基本类型都有一个与之对应的类。这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Byte, Short, Integer, Long, Float, Double(这六个类派生自公共的超类Number), Character, Void和Boolean。注意:对象包装器类是不可变的,即一旦构造了包装器,就不容许更改包装在其中的值。同时对象包装器类仍是final的,所以不能定义他们的子类。

※,数组列表ArrayList的泛型参数是不容许为基本类型的,即ArrayList<int> al = new ArrayList<>();是不合法的。此时就须要用到包装器类Integer。注意:因为每一个值分别包装在对象中,因此ArrayList<Integer>的效率远低于int[]数组。所以应该用它构造小型集合,其缘由是此时程序员操做的方便性要比执行效率更加剧要

※,自动装箱(autoboxing)与自动拆箱(unbox): ArrayList<Integer> list = new ArrayList<>();

  • list.add(3);// 此时自动变换为: list.add(Integer.valueOf(3)); 这种变换称为自动装箱。autoboxing这个词来源于C#,在Java中或许自动包装(autowrapping)这个词更合适.
  • 相反地,若是将Integer对象赋值给int值时,将会自动拆箱。即:int n = list.get(i);//翻译为 int n = list.get(i).intValue();
  • Integer n = 3; n++;//编译器将自动地插入一条对象拆箱指令,而后进行自增计算,最后再将结果装箱。

※,关于包装器类的几个注意事项:

  • 比较两个包装器类通常使用equals()方法,【==】比较两个包装器对象时,检测的是两个对象是否指向同一个存储区域。
    • 所以 Integer a = 128; Integer b = 128; a==b;// 返回false。
    • 可是注意,Java中自动装箱规范要求boolean, byte, char<=127, 介于[-128~127]之间的short和int被包装到固定的对象中。因此若是 Integer a = 127; Integer b = 127; a==b;//此时返回true。
  • 当Integer n = null 时, 3 * n; 会报空指针异常。
  • 若是一个表达式中混合使用Integer 和Double类型,Integer值就会自动拆箱,提高为double,而后再装箱为Double。Integer n = 1; Double x = 2.0; sout(true? n : x);//打印1.0。
  • // 注意调用triple(n)方法并不能将n变成3倍。由于包装器类Integer是不可变的!
    public static void triple(Integer x) {
        x = x * 3; }
  • org.omg.CORBA定义的IntHolder等类型能够用于编写修改数值参数值的方法。public static void triple(IntHolder x) {x.value = 3 * x.value;}

※,装箱和拆箱操做是编译器的行为,而不是Javad虚拟机的行为。编译器在生成类的字节码时插入必要的方法调用。虚拟机只是执行这些字节码。

※,数值对象包装器的另外一个好处是: Java设计者发现能够将某些基本方法放置在包装器类中,如 int x = Integer.parseInt(string);// parseInt()是一个静态方法,这与Integer对象毫无关系。可是Integer类是放置这个方法的好地方。

※,API---java.lang.Integer 1.0

  • int intValue();//返回Integer对象的int值。覆盖了Number类中的intValue()方法。
  • static String toString(int i);// 
  • static String toString(int i, int radix);//radix指明了第一个参数的进制,即 i 是radix进制的数值。
  • static int parseInt(String s);
  • static int parseInt(Strin s, int radix);// radix做用同上。
  • static Integer valueOf(String s);
  • static Integer valueOf(String s, int radix);// radix 做用同上。

26,参数数量可变的方法

※,在JavaSE5.0以前,每一个Java方法都是固定参数个数。以后有了变参方法。一个例子就是PrintStream System.out.printf()方法。这个方法能够传入任意个数的参数。这个方法的实现以下:

// ...语法表示这个方法能够接收任意数量的对象。
public PrintStream printf(String format, Object ... args) {
    return format(format, args); } 1, 实际上,printf()方法接收2个参数,一个是格式化字符串,另外一个是Object[]数组,这个数组中保存着全部的参数(若是调用者提供的是整形数组或其余基本类型的值,自动装箱功能将把它们转换成对象)。 2,就是说,Object ... 和 Object[]彻底同样。

※,一个自定义的可变参数的方法:参数类型能够任意,甚至能够为基本类型。

// 
public static double max(double... values) {
    double largest = Double.NEGATIVE_INFINITY; for (double v : values) { f (v > largest) { largest = v; } } return largest; } 1, 调用方式: max(3.1, 4.5, -5); 2, 编译器将new double[] {3.1, 4.5, -5}传递给max方法。也能够直接这么调用max方法(即传入一个数组),可是注意要保持类型的一致。
好比:public static void f(Object...args){System.out.println(Arrays.toString(args));},若是以数组参数形式调用此方法,那么这个数组必须是Object[]才能保证一一对应。
假如传入的是int[]{1,2,4},那么这个int[]数组总体将被当作是Object[]中的一项。即打印出来的是【[[I@15db9742]】,而若是传入的是Object[]{1,2,4}那么打印出来的即是[1,2,4]。

※,在Java中已经存在且最后一个参数是数组的方法能够重定义为可变参数的方法而不会破坏任何已经存在的代码。好比大名鼎鼎的main方法就能够写为: 

    public static void main(String ... args) {...}

※,

※,

27,枚举类(Enumeration Classes)

※,JDK1.5以后出现了enum类型。能够单独成类,也能够定义在class或interface之中。

※,枚举的用法:

/**
 * 1,枚举enum是一个特殊的Java类。它继承自java.lang.Enum。枚举类是final类,不能被继承。 
 * 2,经过 cfr jar包反编译能够发现,其实enum就是一个语法糖。
 * 3,RED, GREEN, BLUE(叫作枚举常量)实际上就是枚举类RGB的实例,外部没法再构造出RGB的实例。所以,再比较两个
 *    枚举类型的值时,不须要调用equals()方法,直接使用“==”就能够了。
 * 4,枚举的构造器只是在构造枚举常量的时候被调用,即RED,GREEN,BLUE各调用一次。因此,枚举常量的
 *    形式要和构造函数保持一致。好比RED(1, "红色")须要对应RGB(int a, String b){}形式的构造函数。
 *    能够存在多个构造函数,所以也能够存在多种形式的枚举常量,好比:RED(1,"红色"),GREEN, BLUE;枚举的
* 构造器只能用private修饰,若是没有修饰符,默认也是private。 * 5, 枚举常量要放在最前面,枚举的域和方法要放在枚举常量的后面。这个不明白为啥..
* 6, 能够在枚举类中覆盖超类Enum的一些方法,好比@Override public String toString(){return ...;} * */ enum RGB { RED(), GREEN, BLUE; } // 反编译后的代码以下: java -jar ../cfr_0_132.jar RGB.class --sugarenums false /* * Decompiled with CFR 0_132. */ public final class RGB extends Enum<RGB> { public static final /* enum */ RGB RED = new RGB(); public static final /* enum */ RGB GREEN = new RGB(); public static final /* enum */ RGB BLUE = new RGB(); private static final /* synthetic */ RGB[] $VALUES; public static RGB[] values() { return (RGB[]) $VALUES.clone(); } public static RGB valueOf(String string) { return Enum.valueOf(RGB.class, string); } private RGB() { super(string, n); } static { $VALUES = new RGB[] { RED, GREEN, BLUE }; } }

※,枚举类的一些方法:java.lang.Enum<E> 5.0     RGB r = RGB.RED;

  • String toString();// 返回枚举常量名。 r.toString(); // RED;
  • int ordinal();// 返回枚举常量在enum声明中的位置,位置从0开始计数。 r.ordinal();//0
  • int compareTo(E other);// 若是枚举常量的ordinal在other以前,返回负值;若是this == other,返回0;不然返回负值。 r.compareTo(RGB.GREEN);// -1
  • Class<E> getDeclaringClass();//返回枚举常量所在枚举类的类对象。r.getDeclaringClass();// class com.learn.java.RGB
  • String name();// 枚举常量名。r.name();// RED;
  • static E[] values();//返回一个包含所有枚举常量的的数组。RGB[] values = RGB.values();// 
  • static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name);// 返回指定枚举常量名指定枚举类型的枚举常量。RGB g = Enum.valueOf(RGB.class, "GREEN");

※,枚举与switch

RGB r = RGB.BLUE;
switch (r) { case RED://这里只能用RED,不能用RGB.RED System.out.println("rgb.red"); break; case BLUE: System.out.println("rgb.blue"); break; default: System.out.println("rgb..."); } 

※,

28,反射(Reflection) 

※,可以分析类能力的程序称为反射。也就是说,反射的代码研究的是类自己,有点元数据(meta-data)的感受。反射是一种功能强大且复杂的机制 。 使用它的主要人员是工具构造者, 而不是应用程序

员。

※,Class 类:描述类的一个类。

※,获取Class类的对象的三种方法:

  • Employee e = new Employee();   Class c = e.getClass();
  • Class c = Class.forName("java.util.Random");// 静态方法forName()的参数只有是类名或接口名时才可以执行,不然,forName()方法将抛出一个异常。
  • Class c = Employee.class; Integer.class; int.class; Void.class; void.class; 等等。

    Class类其实是泛型类。实际上应该写为Class<Employee> c = Employee.class;

※,API: java.lang.Class 1.0

  • String getName();// 返回类的名字
  • static Class forName(String className);//返回一个Class对象。
  • Object newInstance(); // 返回这个类的一个新实例。如: e.getClass().newInstance();将返回一个与e具备相同类型的的实例。newInstance()方法调用默认的(即没有参数的)构造器初始化新建的对象,若是这个类没有默认的的构造器就会抛出异常。

※,java.lang.reflect包中有三个类Filed, Method, Constructor 分别用于描述类的域、方法和构造器。里面的一些方法有须要后面再研究。

※,

29,继承的设计技巧

※,将公共操做和域放在超类。

※,尽可能不要使用protected 域。

  • 子类集合是无限制的,任何一我的均可以由某个类派生出一个类子类,并编写代码以直接访问protected的实例域。这破坏的封装性。
  • 在Java中,同一个包中的全部类均可以访问protected域,而无论它是不是这个类的子类。
  • 不过,protected方法对于指示那些不提供通常用途而应该在子类中从新定义的方法颇有用。

※,除非全部继承的方法都有意义,不然不要使用继承。

  • 假设想编写一个Holiday类,若是继承GrigorianCalendar,那么GregorianCalendar中的add()方法能够将某个holiday变为非holiday。所以继承GregorianCalendar不合适。
  • 可是继承LocalDate就没有这个问题,由于LocalDate类是不可变的,没有任何方法能够把假日变成非假日。

※,使用多态而非类型信息。使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展

※, 不要过多的使用反射。反射功能对于编写系统程序来讲极其实用, 可是一般不适于编写应用程序。

第六章: 接口 、 lamda表达式 与内部类

一,接口

1,Java中,接口不是类,而是对类的一组需求描述。

2,一个例子: java.util.Arrays.  static void sort(Object[] a) ,能够对数组a中的元素进行排序。前提是数组中的元素必须属于实现了 Comparable 接口的类,而且元素间是可比较的。

// Comparable接口(是个泛型接口)源码以下:
public interface Comparable<T> {
    public int compareTo(T o); } 
// 语言标准规定: x.compareTo(y)和y.compareTo(x)一定是相反的。若是一个抛异常,另外一个也应该抛异常。因此涉及子类继承时,须要判断是否子类和超类能够比较分两种状况处理。 // 状况1,若是子类和超类没法对比,那么就加上getClass类型判断 class Employee implements Comparable<Employee> { public int compareTo(Employee e) { if (getClass() != e.getClass()) { throw new ClassCastException(); } /** * java.lang.Double/Integer * static void int compareTo(double x, double y) * x < y 返回负值,相等返回0,x > y返回正值 */ return Double.compare(salary, e.getSalary()); } } // 状况2,若是子类和超类能够比较,那么在超类中提供一个compareTo()方法并声明为final class Employee implements Comparable<Employee> { public final compareTo(Employee e) { return Double.compare(salary, e.getSalary()); } }
  • 接口不是类,不能实例化。可是能够声明一个接口类型的变量,条件是这个变量必须引用实现了这个接口的类对象。如:Comparable x = new Employee();//Employee必须实现Comparable接口。
  • instanceof除了用于检查某个类是否属于某个特定类,也能够用来检测一个对象是否实现了某个特定的接口。if (anObject instanceof Comparble) {...}.
  • 同类同样,接口也能够也能够被继承。使用extends(英文是拓展的意思,仔细品味一下)关键字。
  • 接口中全部的方法自动地属于public abstract (其中的abstract关键词不包含后面讲的静态方法和默认方法),全部的域自动的属于public static final(静态常量)。因此在声明接口时,能够没必要添加这些关键字(Java规范也推荐不添加这些多余的关键字)。
  • 每一个类只能有一个超类, 可是能够实现多个接口。一个接口能够继承(拓展)多个接口。接口与抽象类的区别就在于类的只能够单继承与接口的能够多实现。C++支持多继承。

3,接口中不能含有实例域,由于接口不能实例化。在Java SE 8 以前,也不能在接口中实现方法。可是在Java SE 8中,能够实现静态方法和默认方法了。

  • 关于静态方法: 一般的作法是将静态方法放置在伴随类中,在标准库中有不少成对出现的接口和实用工具类,好比:Collection / Collections; Path / Paths。
    /**
     * 在Java SE 8中,能够为Path接口增长静态方法,这样一来,Paths实用工具类就再也不是必要的。
     * 不过,整个Java库都以这种方式重构不太可能,可是在实现咱们本身的接口时,能够再也不提供一个伴随类了。
     */
    public interface Path
    {
        public static Path of(URI uri) { . . . } public static Path of(String first, String... more) { . . . } . . . }
  • 关于默认方法: Java SE 8中还能够为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。
    /**
     * Java SE 8中能够为接口方法提供一个默认实现,实现体中能够调用任何其余方法。
     * 有了默认方法,实现这个接口的类就能够没必要实现这个方法了,
     * 若是没实现这个方法的类的实例对象调用了这个方法就会调用接口中定义的这个默认方法。
     */
    public interface Collection
    {
        int size(); // an abstract method
        default boolean isEmpty() { return size() == 0; } . . . }
  • 解决默认方法冲突:
    • 若是某个类A实现了2个接口B,C,其中一个提供了一个默认方法func(),若是另一个接口也有一个同签名的方法,那么不管另一个接口中的这个方法是否实现了,则编译器会报二义性错:默认方法冲突了,类A必须覆盖这个方法来解决冲突:或者类A实现本身的func()方法,或者指定一个接口中的默认方法,语法格式是B.super.func();即(接口名.super.方法名)。
    • 若是某个类A继承了超类B,实现了接口C,而类B和接口C中都含有一个同签名的方法,则遵循“类优先(class wins)”原则,即类A始终继承超类B的这个方法,不管接口C是否实现了这个方法(即默认方法)都不会带来什么影响。

4,更多例子:

※,接口与回调。

/**
 * javax.swing包里的Timer类有一个定时器方法:new Timer(int delay, ActionListener listener)。
 * 将对象传递给定时器,要求此对象必须实现java.awt.event包中的ActionListener接口。
 */
ActionListener listener = new TimePrinter(); Timer t = new Timer(1000, listener);//每隔1000ms调用一下listener中的actionPerformed方法。 t.start();
/**
* 在关闭提示框以前,每隔1s执行一下。若是没有下面这句代码,程序注册了事件以后就退出了,因此不会出现预期的间隔性效果。
* 除了下面这种方式外,还能够使用 java.lang. Thread.sleep(10000);阻止程序退出
*/ JOptionPane.showMessageDialog(null, "Quit Program?");class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen())); // System.out.println("At the tone, the time is " + new Date()); Toolkit.getDefaultToolkit().beep(); } }

javax.swing.JOptionPane 1.2

  • static void showMessageDialog(Component parent, Object message);// 显示一个包含一条消息和OK按钮的对话框。这个对话框将位于其parent组件的中央。若是parent为mill,对话框将显示在屏幕的中央

java.awt.Toolkit 1.0

  • static Toolkit getDefaultToolkit();// 得到默认的工具箱。工具箱包含有关GUI环境的信息。
  • void beep();//发出一声铃响。

※,Comparator 接口:(注意不是 上面的 Comparable 接口,而是 Comparator 接口)

  • 上文已讲,Arrays.sort(object[] a) 方法能够对数组a进行排序,前提是数组a中的元素必须是实现了Comparable接口。String类是实现了Comparable<String>接口的,String类实现Comparable<String>接口的的compareTo(String anotherString)方法的方式是按字典顺序比较字符串。
  • 假设如今但愿按照字符串的长度来排序,则能够使用Arrays.sort()的另外一个版本:Arrays.sort(Object[] a, Comparator c);传入一个数组和一个比较器做为参数,比较器是实现了Compartor<T>接口的对象。
    String[] srr = new String[]{"helloWorld", "bcde","abc","fuck", "good", "fuckU"};
    
    System.out.println(Arrays.toString(srr));//[helloWorld, bcde, abc, fuck, good, fuckU]
    Comparator<String> lengthComparator = new LengthComparator(); Arrays.sort(srr, lengthComparator); System.out.println(Arrays.toString(srr));//[helloWorld, fuckU, bcde, fuck, good, abc] class LengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { /**  * 返回1或正数(true)表示按照这个规则须要调换o1和o2的位置,即o2排在o1的前面; * 返回-1或负数(false)表示不须要调换o1和o2的位置,即o1排在o2的前面。 * 0 表示不排序,同-1. */ return o1.length() - o2.length();// 升序 /** * 若是o1.length > o2.length,返回正数,表示须要调整o1,o2,即o2在o1前面,即升序. * 若是o1.length < o2.length,返回负数,表示不准调整o1,o2,即o1在o2前面,即升序. */ } }

※,对象克隆(Cloneable接口):有关克隆的细节技术性很强,克隆没有你想象中那么经常使用。标准库中只有不到5%的类实现了clone()方法。

  • 若是将一个对象引用(即变量)赋值给另一个变量,那么这两个变量(对象引用) 都指向同一个对象,是同一个对象的引用,任何一个变量改变都会影响到另外一个变量。
  • 使用clone()方法能够克隆一个对象。clone()方法是Object类的一个protected方法,因此和Object不在同一个包(java.lang包)的其余类的对象都没法直接调用这个clone()方法(这是protected修饰符的限制,见上面相关内容)。其余类要想使用这个方法必须实现Cloneable接口【这个Cloneable接口比较特别,它是Java提供的一组标记接口(tagging interface)或叫记号接口(marker interface)。像Comparable<T>等接口的一般用途是确保一个类实现一个或一组特定的方法。可是标记接口中不包含任何方法,它惟一的做用就是容许在类型查询中使用 instanceof :if (anObject instanceof Cloneable){...} 】。
  • Object类中的clone()方法属于“浅拷贝”:
    Employee origin = new Employee();
    Employee cloned = origin.clone(); 浅拷贝:即对象origin中若是有其余引用对象,则cloned对象中并无克隆这个内部的引用对象。即origin对象和cloned对象依然共享一些信息。
  • 一个类实现Cloneable接口的时候,若是Object类中的clone()方法的浅拷贝能够知足要求,那么实现Cloneable接口的时候能够以下:
    class Employee implements Cloneable {
        @Override
        public Employee clone() throws CloneNotSupportedException { return (Employee) super.clone(); } }
  • 若是Object类中的浅拷贝clone()方法没法知足要求,那么能够本身实现深拷贝,代码以下:
        /**
         * 抛出异常,之因此不捕获这个异常,是由于捕获异常很适合用于final类。
         * 若是不是final类最好仍是保留throws符号。这样就容许子类不支持克隆时
         * 能够选择抛出一个CloneNotSupportedException异常。
         */
        @Override
        public Employee clone() throws CloneNotSupportedException { //调用 Object.clone() Employee cloned = (Employee) super.clone(); // 克隆对象中的可变域 cloned.birthDay =(Date) this.birthDay.clone(); return cloned; }
  • 全部数组类型都有一个public 的clone()方法,而不是protected。能够用这个方法克隆一个新数组,包含原数组全部元素的副本。
    int[] arrA = {2, 3, 5, 7, 11};
    int[] cloned = arrA.clone(); System.out.println(Arrays.toString(arrA));//[2, 3, 5, 7, 11] cloned[0] = 33;//不会改变arrA数组  System.out.println(Arrays.toString(arrA));//[2, 3, 5, 7, 11] System.out.println(Arrays.toString(cloned));//[33, 3, 5, 7, 11]
  • 卷II的第2章将展现另外一种克隆对象的机制,其中使用了Java的对象串行化特性。这个机制很容易实现,并且很安全,但效率不高。

二, lambda表达式:

5,语法格式: 

  • //这是彻底体形式,下面有各类特殊形式
    Arrays.sort(srr, (String o1, String o2) -> { return o1.length() - o2.length(); });
  • 无参数时或2个及以上参数时须要使用一对圆括号()。
  • 一个参数时能够省略圆括号。
  • 参数类型能够推断出来,无需显式指定,固然也能够显式指定。
  • 方法体若只有一行代码,则能够不用大括号{},此时有返回值也不能用 return 关键字。如:Arrays.sort(arr, (first, second) -> first.length() - second.length());//方法体没有大括号,没有分号,没有return。
  • 方法体若超过一行代码,则必须使用大括号{},方法体内若是有返回值须要有return关键字,固然一行代码也能够使用这种形式。如:
                Arrays.sort(srr, (o1, o2) -> {
                    return o1.length() - o2.length(); });

6,函数式接口(Functional Interface)

  • 当且仅当一个接口中只含有一个抽象方法时,这个接口叫作 函数式接口。Java SE 8专门引入了一个注解 @FunctionalInterface。该注解用于接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,不然将会报错。须要注意的是,即便不使用该注解,只要知足函数式接口的定义,这仍然是一个函数式接口,使用起来都同样。
  • 函数式接口的特殊之处在于能够使用lambda表达式代替这种接口的对象。如Comparator<T>接口只有一个抽象方法,属于函数式接口(下面专门再讲了Comparator接口,里面实际有两个抽象方法,可是依然属于函数式接口,具体见下面再谈Comparator接口)。因此Arrays.sort(Object[] a, Comparator<T> c)中的第二个参数除了能够使用一个Comparator<T>对象以外,还能够直接传入一个lambda表达式。上面已经这么使用过了。
  • 实际上,在Java中,对lambda表达式所能作的也只是能将其转换为函数式接口。即只能用函数式接口类型接受一个lambda表达式。甚至都不能用Object来接受一个lambda表达式,由于Object不是一个函数式接口。
  • java.util.function包中定义了不少很是通用的函数式接口。一个尤为有用的函数式接口是Predicate<T>接口。ArrayList类有一个removeIf()方法,它的参数就是一个Predicate<T>。
    public interface Predicate<T> {
        boolean test(T t);
        // additional default and static methods
    } ArrayList<Integer> list = new ArrayList<>(); list.removeIf(e -> e == null);//将list中全部为null的元素删除掉。  等价于如下: list.removeIf(new RemoveCond()); class RemoveCond implements Predicate<Integer> { @Override public boolean test(Integer t) { return t == null; } } 

7,方法引用(Method Reference)

  • Timer timer = new Timer(1000, event -> System.out.println(event));
    /**
     * object::instanceMethod
     * 表达式System.out::println是一个方法引用,等价于 event -> System.out.println(event)
     */ Timer timer = new Timer(1000, System.out::println); String[] srr = {"Abc", "abbbbb", "good", "Goaaaa"}; //第二个参数是函数式接口 Comparator<String> 的一个实现,表示不考虑字母大小写对字符串数组排序 Arrays.sort(srr, (x, y) -> x.compareToIgnoreCase(y)); /** * Class.instanceMethod * String::compareToIgnoreCase是一个方法引用,等价于(x, y) -> x.compareToIgnoreCase(y) */ Arrays.sort(srr, String::compareToIgnoreCase); //关于方法引用 用“::”操做符分隔方法名 与 对象或类名。主要有三种状况(方法引用貌似均可以替换为lambda表达式): 1. object::instanceMethod,如System.out::println等价于 x->System.out.println(x); System.out是PrintStream类的一个对象 2. Class::staticMethod,如 Math::pow等价于Math.pow(x, y); 3. Class::instanceMethod,如String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y);第一个参数会成为方法的隐式参数 //另外 1. 能够在方法引用中使用this。this::equals等价于x -> this.equals(x); 2. 也能够在方法引用中使用super。super::instanceMethod会使用this做为隐式参数,调用给定方法的超类版本。

8,构造器引用(Constructor Reference)

  • /**
     * 构造器引用和方法引用很类似,只不过方法名为new。好比Employee::new是Employee的构造器的一个引用。
     * 注意:每一个stream流只能用一次,不然会报错:stream has already been operated upon or closed
     */
    //如今将一个字符串列表转换为Employee对象数组列表
    List<String> names = Arrays.asList("Liverpool", "Kloop", "Chamberlain", "Mane", "Salah", "Firmino");
    Stream<Employee> stream = names.stream().map(Employee::new);//map方法会为names中每一个元素调用Employee(String name)构造器
    List<Employee> staff = stream.collect(Collectors.toList()); //能够使用数组类型创建构造器引用。例如int[]::new 是一个构造器引用,它有一个参数即数组的长度。等价于lambda表达式:x -> new int[x] Stream<Employee> stream1 = names.stream().map(Employee::new); Object[] staff1 = stream1.toArray(); Stream<Employee> stream2 = names.stream().map(Employee::new); Employee[] staff2 = stream2.toArray(Employee[]::new);

9,lambda表达式的变量做用域

  •     public static void repeatMessage(String text, int delay) {
            /**
             * 1,下面的lambda表达式由3个部分组成:
             * ①参数event
             * ②代码块
             * ③自由变量,指的是非参数event并且不在lambda代码块中定义的变量。这里便是text变量
             * 
             *,2,表示lambda的数据结构必须存储自由变量的值,咱们说自由变量的值被lambda表达式捕获(captured)了。
             * 代码块加上自由变量的值构成了一个闭包。在Java中,lambda表达式就是闭包。
             * 
             *,3,lambda表达式中捕获的变量必须是常量或者其实是常量(final or effectively final)。所谓其实是常量
             * 意思是,这个变量初始化以后就不会再为它赋值。因此下面两处给text再赋值的操做编译器都会报错。这个限制是有缘由的:
    * 若是在lambda表达式中改变变量,并发执行多个动做时就会不安全。 * *,4,lambda表达式嵌套在 repeatMessage 方法中,lambda表达式的做用域和嵌套块有相同的做用域。因此下面在嵌套块 * 中定义event变量就会和lambda表达式中的event变量冲突,编译器报错。 * *,5,由上面4中所述可推知,lambda表达式中若是使用了this关键字,这个this和在嵌套快中的this彻底同样,指的就是 * 实例化的对象。 */ //5,System.out.println(this.toString());// lambda表达式中如有this关键字,和此处的this含义相同。 //4, String event = "xxx";//lambda表达式的做用域和此处的event有相同的做用域,因此会报变量名已定义的编译错误。 //3 text = "从新赋值"; ActionListener listener = event -> { //3 text = "从新赋值"; System.out.println(text); Toolkit.getDefaultToolkit().beep(); }; new Timer(delay, listener).start(); }

10,经常使用的函数式接口:略

11,再谈Comparator接口:

  •  Comparator接口里实际上有两个抽象方法,除了 int compare(T o1, T o2); 还有一个抽象方法:boolean equals(Object obj);可是Comparator依然属于函数式接口。public @interface FunctionalInterface的官方文档以下:
    • If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count  since any implementation of the interface will have an implementation from  java.lang.Object or elsewhere. 翻译以下:
    • 若是一个接口声明了一个抽象方法, 这个抽象方法覆盖了java.lang.Object的public方法,那么这个接口的这个抽象方法不会被计入到接口的抽象方法数量中,由于任何实现这个接口的类都会从java.lang.Object 或者 其余类 继承这个抽象方法的实现。(正是因为上面讲到的 类优先"class wins"原则。)
  • <T extends Comparable<? super T>> 含义解释(点我):泛型T的上限是Comparable<? super T>,<? super T>表示Comparable的泛型的下限是T,即?是T的超类(接口)。
  • Comparator中有不少静态方法能够方便的建立比较器,一些用法以下:comparing()方法的第一个参数是键提取器。
    • 假设有一个Employee对象数组 staff ,能够按以下按名字对这些对象排序: Arrays.sort(staff, Comparator.comparing(Employee::getName));实际使用中发如今调用reversed()方法时 Comparator.comparing(Employee::getName).reversed()正常,可是换成lambda表达式就报错 Comparator.comparing(x -> x.getName()).reversed(),暂不明白缘由。
    • 若是名字同样,按照年龄排序:Arrays.sort(staff, Comparator.comparing(Employee::getName).thenComparing(Employee::getAge)));
    • Comparator.comparing()还有一个变体,能够传入第二个参数为第一个参数指定一个自定义的比较器,好比按照名字的长度排序:
      • Arrays.sort(staff, Comparator.comparing(Employee::getName, (s, t) -> Integer.compare(s.length(), t.length())));
    • comparing()和thenComparing()方法还有不少变体,能够避免int, double, long值的装箱,好比上面那个操做能够简化为:
      • Arrays.sort(staff, Comparator.comparingInt(p -> p.getName().length()));
    • 若是comparing()的第一个键提取器提取的键值能够为null,就须要用到nullsFirst()和nullsLast()方法了(返回值是一个比较器,参数也是一个比较器)。这两个静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于(nullsFirst())或大于(nullsLast())正常值。例如,若是名字为null就排在最前面能够以下使用:
      • Arrays.sort(staff, Comparator.comparing(Employee::getName, Comparator.nullsFirst(Comparator.naturalOrder())))); // naturalOrder()是另外一个静态方法,返回一个比较器。
      • Comparator.nullsFirst(Comparator.naturalOrder().reversed()); // 等同于 Comparator.nullsFirst(Comparator.reverseOrder());

三,内部类(Inner Class)

 内容挺多,暂不记录了。之后再看一遍在记录

※,匿名内部类:

 

四,Service Loader(动态加载实现接口的全部类)参见此篇文章 

1,用法:只需将全部类文件打包到一个jar文件中,而后在jar文件中的 META-INF文件夹下新建一个services文件夹,services文件夹下新建一个文件,文件名为接口的全名(即带着包名,如com.learn.java.HelloInterface)。文件的内容是实现这个接口的全部类的全名。而后使用ServiceLoader类就能够今后文件中读取到全部实现该接口的类了。详细细节以下。

  • com.learn.java包下定义一个接口
    package com.learn.java;
    
    public interface HelloInterface {
        void sayName();
        void sayAge();
    }
  • tong.huang.shan包下定义两个实现该接口的类
    package tong.huang.shan;
    
    import com.learn.java.HelloInterface;
    
    public class Dog implements HelloInterface {
    
        public static void main(String[] args) {
            System.out.println(String.format("%s: main方法", Dog.class.getName()));
        }
        @Override
        public void sayName() {
            System.out.println(String.format("%s: 汪汪", this.getClass()));
        }
    
        @Override
        public void sayAge() {
            System.out.println(String.format("%s: 3岁", this.getClass()));
        }
    
    }
    package tong.huang.shan;
    
    import com.learn.java.HelloInterface;
    
    public class Sheep implements HelloInterface {
        @Override
        public void sayName() {
            System.out.println(String.format("%s: 咩咩", this.getClass()));
        }
        @Override
        public void sayAge() {
            System.out.println(String.format("%s: 10岁了", this.getClass()));
        }
    }
  • tong.huang.shan包下新建一个测试类
    package tong.huang.shan;
    
    import java.util.ServiceLoader;
    
    import com.learn.java.HelloInterface;
    
    public class Study {
        public static void main(String[] args) {
            /**
             * 下面这行代码是在静态方法中获取类名的方法。因为getClass()方法须要this来调
             * 用,可是静态方法中没有this,因此在静态方法中构造一个匿名内部类
             * 【new Object(){},匿名内部类是这个类(此例中即Object)的子类】,
             * 获取此匿名内部类的class类(class tong.huang.shan.Study$1),
             * 而后再调用getEnclosingClass()方法获取包围这个内部类的类,
             * 即此静态方法的class类(class tong.huang.shan.Study)。
             */
            System.out.println(new Object(){}.getClass().getEnclosingClass() + ":");
            ServiceLoader<HelloInterface> serviceLoader = ServiceLoader.load(HelloInterface.class);
        
            for (HelloInterface myServiceLoader : serviceLoader) {
                myServiceLoader.sayAge();
                myServiceLoader.sayName();
            }
        }
    }
  • 进入包的第一层所在目录(com以及tong所在的目录),使用命令 jar cvfe D:\loader.jar tong.huang.shan.Study '.\tong\huang\shan\Study$1.class' .\tong\huang\shan\Study.class .\tong\huang\shan\Sheep.class .\tong\huang\shan\Dog.class .\com\learn\java\HelloInterface.class;这个命令将全部相关class文件打到一个jar包里。
  • 而后打开jar包,在META-INF文件夹下新建services文件夹,在services文件夹下新建 com.learn.java.HelloInterface 文件,文件内容为:

    tong.huang.shan.Dog
    tong.huang.shan.Sheep

  • 而后 运行此jar包:java -jar D:\loader.jar 便可看到 serviceLoader读取到了全部实现HelloInterface的类。

2, API

  • java.util.ServiceLoader<S> 1.6
    • static <S> ServiceLoader<S> load(Class<S> service);//建立一个service loader,这个loader将会加载实现了给定接口的全部类。

    • Iterator<S> iterator();
    • Stream<ServiceLoader.Provider<S>> stream() 9;//JDK 9才有的方法。
    • Optional<S> findFirst() 9;// JDK 9才有。
  • java.util.ServiceLoader.Provider<S> 9;//JDK 9才有
    • Class<? extends S> type(); //gets the type of this provider.
    • S get(); //gets an instance of this provider.

3, 

五,代理类(Proxy)

1,具体不是很理解,记录几个例子。

2, 利用代理能够在运行时建立一个实现了一组给定接口的新类。这种功能只有在编译时没法肯定须要实现哪一个接口时才有必要使用。

3,建立一个代理对象,须要使用 java.lang.reflect.Proxy 类的newProxyInstance() 方法:

  • Object java.lang.reflect.Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

4,一个例子: 使用代理类 和 调用处理器  跟踪方法调用。

  • 先定义一个 调用处理器。定义一个TraceHandler包装器类存储包装的对象。其中的invoke()方法打印了被调用方法的名字和参数,随后用包装好的对象做为隐式参数调用这个方法。
    package tong.huang.shan;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class TraceHandler implements InvocationHandler {
        private Object target;
        public TraceHandler(Object t) {
            target = t;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            System.out.print(target);
            System.out.print("." + method.getName() + "(");
            if (null != args) {
                for (int i = 0; i < args.length; i++) {
                    System.out.print(args[i]);
                    if (i < args.length -1) {
                        System.out.print(",");
                    }
                }
            }
            System.out.println(")");
    
            return method.invoke(target, args);
        }
    }
  • 假设如今 使用代理对象对二分查找进行跟踪,代码以下
    package tong.huang.shan;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.Random;
    
    import com.learn.java.HelloInterface;
    
    public class Study {
        public static void main(String[] args) {
            // Object value = "红楼梦";
            // InvocationHandler handler = new TraceHandler(value);
    
            // Class[] interfaces = new Class[] {Comparable.class};
            // Object proxy = Proxy.newProxyInstance(null, interfaces, handler);
            // Class pc = Proxy.getProxyClass(null, interfaces);
    
            // System.out.println(proxy.getClass());
            // System.out.println(value.getClass());
            // System.out.println(Proxy.isProxyClass(proxy.getClass()));
            // System.out.println(Proxy.isProxyClass(value.getClass()));
            // proxy.equals("平儿");
            // value = proxy;
            // System.out.println(value.equals("史湘云"));
    
            Object[] elements = new Object[1000];
            for (int i = 0; i < elements.length; i++) {
                Integer value = i + 1;
                InvocationHandler handler = new TraceHandler(value);
                Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler);
                elements[i] = proxy;
            }
    
            Integer key = new Random().nextInt(elements.length) + 1;
            // key = 500;int result = Arrays.binarySearch(elements, key);// 这句会调用invoke()方法,打印出二分查找的全过程。
    
            if (result > 0) {
                System.out.println(elements[result]);
            }
    
        }
    }
  • 打印结果以下
    500.compareTo(159)
    250.compareTo(159)
    125.compareTo(159)
    187.compareTo(159)
    156.compareTo(159)
    171.compareTo(159)
    163.compareTo(159)
    159.compareTo(159)
    159.toString()//虽然toString()方法不属于Comparable接口,可是toString()方法也被代理了,这是由于Object类中的一部分方法都会被代理。

5,代理类的特性

  • 代理类是在程序运行期间建立的,一旦被建立,就变成了常规类,与虚拟机中的其余任何类没有什么区别。
  • 全部的代理类都重写了Object类中的toString(), equals(),hashCode()方法,和全部的代理方法同样,这些方法也会调用invocation handler中的invoke()方法。可是没有从新定义其余的方法(如clone()和getClass()方法)。
  • 代理类的名字没有定义。可是虚拟机中的Proxy代理类将会产生以$Proxy打头的类名,如com.sun.proxy.$Proxy0
  • 对于特定的类加载器和预设的一组接口来讲,只能有一个代理类。也就是说,若是使用同一个类加载器和接口数组调用两次Proxy.newProxyInstance()方法的话,只可以获得同一个类的两个对象。
  • 能够使用Class proxyClass = Proxy.getProxyClass(null, interfaces)方法获取代理类的Class对象。
  • 代理类必定都是public 和final的。若是代理类实现的全部接口都是public的,那么代理类就不属于某个特定的包;不然,全部非公有的接口都必须属于同一个包,同时,代理类也要属于这个包。
  • 能够经过调用 boolean java.lang.reflect.Proxy.isProxyClass(Class<?> cl)方法检测一个特定的Class对象是否表明一个代理类。

6,API

  • java.lang.reflect.InvocationHandler 1.3
    • Object invoke(Object proxy, Method method, Object[] args); //定义了代理对象调用方法时但愿执行的动做

  • java.lang.reflect.Proxy 1.3
    • static Class<?> getProxyClass(ClassLoaderloader, Class<?>... interfaces);//返回实现指定接口的代理类
    • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler  handler);// 构造实现指定接口的代理类的一个新实例。全部方法会调用给定处理器对象的invoke方法。

    • static boolean isProxyClass(Class<?> cl); // 若是cl是一个代理类则返回true。

7,

第七章,异常、断言和日志

在Java语言中,有三种处理系统错误的机制

  • 抛出一个异常
  • 使用断言
  • 日志

 

一,处理错误

1,异常分类

  • Java中的全部异常对象都是 Throwable类 的实例对象。
  • Throwable类有两个分支:Error类和Exception类。
  • Error类 描述了Java运行时系统的内部错误和资源耗尽错误。若是出现了这样的内部错误,除了通告给用户并尽力使程序安全的终止以外,再也无能为力了。这种状况不多出现。
  • Exception 类又分为两支:RuntimeException 类 和 其余异常 类。划分规则是:由程序错误致使的异常属于RuntimeException;而程序自己没有问题,但因为像I/O错误这类的问题致使的异常属于其余异常(如IOException)。“若是出现RuntimeException异常,那么必定是你的问题了!”
  • Java语言规范将派生于 Error 类 和 RuntimeException 类的全部异常称为 “unchecked exception”,全部其余的异常称为“checked exception”。编译器将会检查是否为全部的checked exception提供了异常处理器。

2,抛出异常时,不须要抛出从Error继承的错误(Error类也算是异常的一种)。任何程序代码都具备抛出那些异常的潜能,而咱们对其没有任何控制能力。

3,子类抛出的异常不能比超类更通用(即子类抛出的异常要么和超类抛出的异常相同要么是超类抛出异常的子类)。特别是当超类没有抛出异常时,子类也不能抛出任何异常。

4,自定义异常类

  • 习惯上,自定义的异常类应该包含两个构造器:一个是默认的无参数构造器,一个是带有详细描述信息的构造器,e.getMessage()就是这里的描述。
    class CustomException extends Exception {
        CustomException() {
        }
    
        CustomException(String msg) {
            super(msg);
        }
    }

5,

二,捕获异常

1,若是try子句中的某一行代码抛出了异常,那么程序将跳过try块中剩余的其他代码。

2,一个try语句块中能够捕获多个异常类型,连续多个catch。

3,Java SE7中,同一个catch子句中能够捕获多个异常类型,只有当捕获的异常类型彼此之间不存在子类关系时才须要这个特性。例如:

  • try {
        .......
    } catch (FileNotFoundException | UnknowHostException e) {
        System.out.println(e.getClass());// 因为一旦抛出一个异常,代码就不继续往下运行了,因此catch时只能catch一个异常,因此用一个变量e。
    }
  • 当在一个catch子句中捕获多个异常时,异常变量e隐含为final变量,没法再对其进行赋值。

4,finally子句:

  • try 语句能够只有finally子句而没有catch子句。try {} finally {}
  • 强烈建议解耦合 try / catch 和 try / finally语句块。这样能够提升代码的清晰度。
    /**
     * 内层的语句块只有一个职责,就是确保关闭输入流。
     * 外层的try语句块也只有一个职责,就是确保报告出现的错误。
     * 这种设计方式不只清楚,并且还具备一个功能,
     * 就是将会报告finally子句中出现的错误。
     */
    
    InputStream in = . . .;
    try {
        try {
            // code that might throw exceptions
        } finally {
            in.close();
        }
    } catch (IOException e) {
        // show error message
    }
  • finally子句中的代码目的是清理资源,因此不要在其中编写影响控制流的代码,如return 语句,throw 语句,break / continue 语句等。因为finally子句中的代码是不管如何都会执行的,因此它可能会覆盖try子句中的代码,好比:
    • 若是finally子句中含有return语句:那么它将会覆盖try中的return 的值。若是try中某个地方抛出了异常,那么finally中的return语句就会将这个异常吞噬掉!
    • 若是finally子句中抛出了一个异常,那么这个异常会覆盖try子句中抛出的异常。若是try中return 一个值,这个值也会被finally中抛出的异常所吞噬。

5, try-with-resources 语句

  • try-with-resources 语句能够自动关闭打开的资源。要求是资源必须是一个实现了AutoCloseable接口或其子接口的类。AutoCloseable接口有一个方法:void close() throws Exception; AutoCloneable接口有一个子接口Closeable接口,这个接口也有一个方法:void close() throws IOException;
  • 语法格式: try (Resource res = ...) {work with res}; try 块正常退出时或者try块中存在异常时,都会自动调用res.close()方法,就像使用了finally 块同样。还能够指定多个资源,例如
    try (Scanner in = new Scanner(new FileInputStream("C:\\Users\\JoY-33\\Desktop\\1.txt"), "GBK");
            PrintWriter out = new PrintWriter("C:\\Users\\JoY-33\\Desktop\\2.txt")) {
        while (in.hasNextLine()) {
            out.println(in.nextLine());
        }
    }
  • 上面已讲过,若是try块中抛出一个异常,并且close()方法也抛出一个异常,close()方法抛出的异常就会覆盖try块中的异常。可是try-with-resources语句能够很好的处理这种状况:try块中的异常会被从新抛出,而close()方法抛出的异常会被抑制(Suppressed)。close()方法抛出的异常将会被自动捕获,并由addSuppressed()方法增长到try块中的异常。能够使用getSuppress()方法获取到从close()方法抛出的全部异常组成的一个数组---->Throwable[]
  • try-with-resources语句也能够有catch子句和finally子句。这些子句会在关闭资源后执行。

6,分析堆栈轨迹元素

  • 堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程当中方法调用的特定位置(包括文件名、类名、方法名、行号信息等)。日常当程序出现未捕获的异常时,控制台打印的就是堆栈轨迹。
  • 堆栈轨迹不止是被动出错时打印出来,也能够主动使用。使用Throwable类的printStackTrace()方法能够主动打印代码的堆栈轨迹,还能够用 Thread.dumpStack() 方法主动打印堆栈轨迹。
    Throwable t = new Throwable();
    t.printStackTrace();
    // 打印结果以下
    java.lang.Throwable
        at tong.huang.shan.Study.main(Study.java:15)

    // 第二个例子
    Throwable t = new Throwable();
    StringWriter out = new StringWriter();
    t.printStackTrace(new PrintWriter(out));
    String desc = out.toString();
    System.out.println(desc);
  • Throwable类还有一个getStackTrace()方法,此方法返回 StackTraceElement数组。能够在程序中分析这个对象数组。例如:
    private static long factorial(int n) {
        System.out.println("factorial(" + n + ")");
        Throwable t = new Throwable(); StackTraceElement[] frames = t.getStackTrace(); for (StackTraceElement frame : frames) {
            System.out.println(frame);
        }
        long r;
        if (n <= 1) {
            r = 1L;
        } else {
            r = n * factorial(n - 1);
        }
        System.out.println("return " + r);
        return r;
    }
    //调用 factorial(3)将打印以下堆栈轨迹:
    
    factorial(3)
    tong.huang.shan.Study.factorial(Study.java:53)
    tong.huang.shan.Study.main(Study.java:32)
    factorial(2)
    tong.huang.shan.Study.factorial(Study.java:53)
    tong.huang.shan.Study.factorial(Study.java:62)
    tong.huang.shan.Study.main(Study.java:32)
    factorial(1)
    tong.huang.shan.Study.factorial(Study.java:53)
    tong.huang.shan.Study.factorial(Study.java:62)
    tong.huang.shan.Study.factorial(Study.java:62)
    tong.huang.shan.Study.main(Study.java:32)
    return 1
    return 2
    return 6
  • 静态的 Thread.getAllStackTrace方法,能够产生全部线程的堆栈轨迹。
    Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
    for (Thread thread : map.keySet()) {
        StackTraceElement[] frames = map.get(thread);
        System.out.println("ThreadName: " + thread.getName());
        System.out.println(Arrays.toString(frames));
    }

7,API

※,java.lang.Throwable 1.0

  • void addSuppressed(Throwable t)  7;//为这个异常增长一个“抑制”异常。这出如今 try-with-resources 语句中,其中的t是close()方法抛出的一个异常。
  • Throwable[] getSuppressd() 7;// 获得这个异常的全部“抑制”异常。通常来讲,这是是 try-with-resources 语句中的close()方法抛出的异常。
  • StackTraceElement[] getStackTrace() 1.4; // 得到构造这个对象时调用的堆栈的轨迹

※,java.lang.StackTraceElement  1.4

  • String getFileName(); // 返回这个元素运行时对应的源文件名。若是这信息不存在,则返回null。
  • int getLineNumber();
  • String getClassName();
  • String getMethodName();// 构造器名是<init> ; 静态的构造器名是<clinit>。没法区分同名的重载方法。
  • boolean isNativeMethod();// 若是这个元素运行时在一个本地方法中,则返回true。所谓native method(本地方法)定义以下
    • A native method is a Java method whose implementation is provided by non-java code.
    • 一个Native Method就是一个java调用非java代码的接口。

※,java.lang.StackWalker 9

※,java.lang.StackWalker.StackFrame 9

8,

三,使用异常机制的技巧

1,能避免异常的尽可能避免异常,由于捕获异常很是耗时。例如用一些判断条件避免异常。例如对一个栈进行退栈操做能够经过判断是否为空规避EmptyStackException。

//使用条件规避异常
if (!s.empty()) {
    s.pop();
}

/**
* 强行退栈并捕获异常很是耗时!!! * 在测试的机器上,调用isEmpty的版本运行时间为646毫秒。 * 捕获EmptyStackException的版本运行时间为21739毫秒。
*/ try { s.pop(); } catch (EmptyStackException e) { }

2,尽可能避免for循环内使用try-catch。而应该try整个for循环。

3,早抛出,晚捕获。

  • 早抛出:在异常出现的源头处就应该抛出异常,好比当栈空时,Stack.pop()能够返回一个null,也能够抛出一个异常。咱们认为在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常要好。
  • 晚捕获:尽可能将异常传递给高层次的方法。让高层次的方法通知用户发生了错误。

4,

四,使用断言

0,不一样的IDE中开启断言(或其余jvm选项)的方法

  • vscode 中开启断言选项的方法:在项目 launch.json文件 中添加 "vmArgs"配置项
    "configurations": [
            {
                "type": "java",
                "name": "CodeLens (Launch) - Study",
                "request": "launch",
                "mainClass": "tong.huang.shan.Study",
                "projectName": "java-study_adaad70d",
                "vmArgs": "-ea",//虚拟机参数
            },
  • IntellijIdea中 能够在 菜单 Run --> Edit Configurations 或直接点击项目列表的下拉菜单中的 Edit Configurations,而后在VM options配置中添加选项。

 

1,在默认状况下,断言机制是被禁用的。能够在运行程序时经过 -enableassertions  或 -ea 选项开启断言,如:java -ea myApp。在启用或禁用断言时没必要从新编译程序。启用或禁用断言是类加载器(class loader)的功能。当断言被禁用时,类加载器将跳过断言代码。

  • 也能够在某个类或某个包中使用开启断言: java -ea: myClass -ea:com.mycompany.mylib myApp
  • 使用 -disableAssertions 或 -da 关闭(部分类、包等)断言。java -ea:...   -da:myClass myApp
  • 有些类不是由类加载器加载,而是直接由虚拟机加载,这些类也能够使用上面选项开启或关闭断言
  • 可是有些“系统类”没有类加载器,-ea再也不适用。对于这些系统类须要使用 -enablesystemassertions或 -esa 选项开启或关闭断言。

2,语法格式:断言有两种格式。若是启用断言,这两种格式都会对条件进行检测,若是为false则抛出一个AssertionError异常。

  • assert 条件;
  • assert 条件 : 表达式; //此种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。

3,说明:

  • 断言检查只用于开发和测试阶段。

4,API:

java.lang.ClassLoader 1.0

  • void setDefaultAssertionStatus ( boolean b ) 1.4
    对于经过类加载器加载的全部类来讲,若是没有显式地说明类或包的断言状态,就启用或禁用断言。

  • void setCIassAssertionStatus ( String className , boolean b ) 1.4
    对于给定的类和它的内部类,启用或禁用断言 。

  • void setPackageAssertionStatus ( String packageName , bool ean b ) 1.4
    对于给定包和其子包中的全部类,启用或禁用断言。

  • void clearAssertionStatus () 1.4
    移去全部类和包的显式断言状态设置 ,并禁用全部经过这个类加载器加载的类的断言。

5,  

五,记录日志(标准Java日志框架)

0,System.out 和 System.err 的区别

  • out为标准输出流,err为标准错误输出流。在idea中控制台中打印的颜色不一样。
  • out在JVM和操做系统中都具备缓存功能,输出的内容不必定实时输出,可能积攒到必定数量才会输出。err则会实时输出。单独使用感受不到,两种方式混合使用便可发现。

1,全局日志记录器:

  • (java.util.logging.)Logger.getGlobal().info("全局记录器打印信息");
  • Logger.getGlobal().setLevel(Level.OFF) 能够取消后续的全部日志输出。

2,自定义日志记录器

  • private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");  与包名相似,日志记录器名也具备层次结构。事实上,与包名相比,日志记录器的层次性更强。对于包来讲,一个包的名字与其父包的名字之间没有语义关系,可是日志记录器的父与子之间将共享某些属性。例如,若是对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别
  • 日志级别按照记录范围大小从小到大依次为: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL。

3,修改日志管理器配置

  • 默认状况下,Java日志只会打印INFO及以上级别的日志,即便设置了Level.ALL也没法打印更低级别的日志。缘由是Java默认的日志配置文件中默认配置是INFO及以上级别。JDK默认的日志配置文件位于 jre/lib/logging.properties (JDK9以后位于conf/logging.properties)。可是Java读取的默认配置文件可能不是这个文件,由于修改了这里面的配置没有生效,手动指定这个修改了的文件却生效了。
  • 能够经过添加jvm参数手动指定日志配置文件  java -Djava.util.logging.config.file=D:\\workware\\jre\\lib\\logging.properties  MainClass  。
  • 配置文件中 能够看到,Java默认的日志处理器(Handler)是ConsoleHandler(还能够设置为FileHandler):handlers= java.util.logging.ConsoleHandler。要想在控制台打印指定级别的日志,须要配置两个地方:① 日志级别: .level=INFO ②控制台输出日志级别:java.util.logging.ConsoleHandler.level = INFO。注意级别都要大写。修改后经过手动指定此文件便可打印任一级别的日志。
  • 还能够在日志配置文件中指定某个 logger 的级别。好比:com.mycompany.myapp.level = SEVERE,便可将 Logger.getLogger("com.mycompany.myapp") 这个日志记录器的级别设为SEVERE。
  • 日志管理器在JVM启动过程当中初始化,这在main执行以前完成。若是在main中调用System.setProperty("java.util_logging.config.file",file),也会调用LogManager.readConfiguration()来从新初始化日志管理器。若是未加JVM参数
    -Djava.util.logging.config.file=D:\\workware\\jre\\lib\\logging.properties 则在main方法中获取系统属性System.getProperty("java.util.logging.config.file") 为null。若是设置了JVM参数,则能够获取设置的值。

4,日志处理器 (P293)

5,日志过滤器 

6,日志格式化器

    public static void main(String[] args) {
        
        Logger logger = Logger.getLogger("tong.huang.shan");
        logger.setLevel(Level.FINE);

  /**
      *
日志记录器会将日志发送到父处理器中,最终的父处理器是一个叫作【""】的处理器,它也有一个ConsoleHandler。若是不设置false则控制台会打印两次
* Logger.getLogger("")能够获取到这个最终的父处理器。
*/ logger.setUseParentHandlers(false); Handler handler = new ConsoleHandler(); handler.setLevel(Level.FINE); handler.setFilter((t)->t.getMessage().contains("33"));//java.log.logging.Filter是一个函数式接口。 handler.setFormatter(new MyFormatter());// java.log.logging.Formatter是一个抽象类。抽象类和接口的区别在于抽象类只能单继承,接口能够多实现。 logger.addHandler(handler); logger.fine("hello world33"); } class MyFormatter extends Formatter { @Override public String format(LogRecord logRecord) { return logRecord.getMessage().replace("33", "55"); } }

 

7,书中有一个自定义处理器(程序清单7.2 p298),经过扩展Handler类或StreamHandler类 实如今窗口中显示日志记录的功能。

8, API

  • java.util.logging.Logger 1.4
    •   
  • java.util.logging.Handler 1.4
  • java.util.logging.ConsoleHandler 1.4
    •     
  • java.util.logging.FileHander 1.4
    •   
  • java.util.logging.LogRecord 1.4
    •   
  • java.util.logging.Filter 1.4
    •   
  • java.util.logging.Formatter 1.4
  •   

六,调试技巧

1,日志代理

        //建立Random的一个匿名子类,重写nextDouble方法,在其中置入日志
        Random random = new Random() {
            public double nextDouble() {
                double result = super.nextDouble();
                Logger.getLogger("").info("nextDouble " + result);
                return result;
            }
        };
        System.out.println(random.nextDouble());;

 

3,堆栈轨迹

  • 通常显示在System.err 流上(new Throwable().printStackTrace()),
    //不带参数的printStackTrace()函数实际的实现以下:
    public
    void printStackTrace() { printStackTrace(System.err); }
  • 也能够将堆栈轨迹发送到一个文件中,代码以下
    PrintWriter pw = new PrintWriter("C:\\Users\\JoY-33\\Desktop\\1.txt");
    new Throwable().printStackTrace(pw);
    /*
     * 注意:使用PrintWriter往文件写入时须要使用flush()或close()将缓冲区刷新,不然不会写入文件中。
     */
    // pw.flush();//仅仅刷新缓冲区,刷新以后流对象还能够继续使用
    pw.close();// 关闭流对象,在关闭以前会先刷新一次缓冲区。关闭以后,流对象不能够继续使用了。
  • 还能够将堆栈轨迹捕获到一个字符串中
                StringWriter out = new StringWriter();
                PrintWriter pw = new PrintWriter(out);
                new Throwable().printStackTrace(pw);
                System.out.println(out);

4,

  • Linux系统中,0表明标准输入(默认从键盘获取输入),1表明标准输出(默认输出到屏幕,即控制台),2表明标准错误(默认输出到屏幕,即控制台)。
  • 如下命令在Linux和Windows的shell中均可运行。
  • java MyProgram > file.txt //程序中的标准输出(System.out)都会重定向到file.txt文件中。实际等同于java MyProgram 1 > file.txt。
  • java MyProgram 2 > file.txt // 程序中的标准错误(System.err)都会重定向到file.txt文件中。
  • java MyProgram > file.txt  2>&1 // 程序中的标准输出和标准错误都会重定向到file.txt文件中。
  • java MyProgram 2>&1 > file.txt和上面效果一致。可是在Linux命令中,两个顺序不一致致使的效果是不同的。

5,

Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler()
{
  public void uncaughtException(Thread t, Throwable e)
{
    //save information in log file
};
});

6,要想观察类的加载过程,能够使用-verbose 参数启动Java虚拟机。有时候这种方法有助于诊断因为类路径引起的问题。

7,属于“lint” 最初是指一种定位C程序中潜在问题的工具。如今泛指 查找可疑可是并不违背语法规则代码的 工具。Java编译器的 -Xlint 选项使用方法以下:

  • 例子: javac -Xlint:fallthrough  /path/to/myClass 
  • 使用 javac -X能够查看全部的-Xlint选项,一些列举以下
  • -Xlint 或 -Xlint:all  执行全部的检查
  • -Xlint:deprecation  和 -deprecation同样,检查废弃的方法。
  • -Xlint:fallthrough  检查switch语句中是否缺乏break语句。
  • -Xlint:finally  警告finally子句不能正常执行。
  • -Xlint:none 不执行任何检查
  • -Xlint:path  检查类路径或源代码路径上的全部目录是否存在。
  • -Xlint:serial    警告没有 serialVersionUID 的串行化类
  • -Xlint:unchecked  对通用类型与原始类型之间的危险转换给予警告

8,Java虚拟机选项中的 -X 选项并无被正式支持,有些JDK选项中并无这些选项。能够使用 java -X 获得全部非标准选项的列表。 javac -X 也同样。

9,jconsole的使用:

  • JDK加载了一个称为jconsole的图形工具,能够用于显示虚拟机性能的统计结果。使用方法是:jconsole processId 。其中的processID便是操做系统中Java虚拟机的进程ID。
  • Java虚拟机进程ID: 在Windows下便是 任务管理器中 java.exe 进程。在Linux下能够使用ps工具找到。
  • 不管 Linux仍是Windows下,均可以使用 jps 查找Java虚拟机进程ID。只输入jps会显示jps进程自己的ID以及java虚拟机进程的ID。jps还有一些参数可选,可自行搜索用法。
  • jsoncole控制台给出了有关运行程序的大量信息。能够参考文档: https://www.oracle.com/technetwork/articles/java/jconsole-1564139.html

10,jmap 的使用:

  • 能够使用jmap实用工具得到一个堆的转储,其中显示了堆中的每一个对象。使用方法以下;(能够使用jmap -help 获取帮助)
  • jmap -dump:format=b,file=out.bin proessId (即Java虚拟机的进程ID)
  • jhat out.bin
  • 上个命令会运行启动一个服务,而后经过浏览器 localhost:7000 查看堆中内容。

11,Java Mission Controller是一个相似于jconsole的专业的分析和诊断Java进程的工具。具体参考文档: https://docs.oracle.com/javacomponents/index.html

12,

第八章:泛型程序设计 (Generic Programming)

一,概述

1,Java SE5.0新增了泛型机制。

2,类型参数(type parameters): 通常 E表示集合元素,K , V表示键值,T, U, S等表示任意类型。

二,泛型类和泛型方法

1,泛型类:Pair<T>

package tong.huang.shan;

// Pair<T>是一个泛型类,也能够说是一个泛型类型(Java中的类也表示一个类型)。在编译器中,这个泛型类被翻译为一个普通的Pair类。
public class Pair<T> { private T first; private T second; Pair() { first = null; second = null; } Pair(T first, T second) { this.first = first; this.second = second; } @Override public String toString() { return "Pair<" + this.first + "," + this.second + ">"; } public T getFirst() { return this.first; } public T getSecond() { return this.second; } public void setFirst(T first) { this.first = first; } public void setSecond(T second) { this.second = second; } }

 

2,泛型方法 和  类型参数的限定类型

package tong.huang.shan;

import java.time.LocalDate;

public class Study {
    public static void main(String[] args) {
        String[] arr = { "mary", "had", "a", "little", "lamb" };
        Pair<String> minmax = ArrayAlg.minmax(arr);
        System.out.println(minmax);
        String middle = ArrayAlg.<String>getMiddle("John", "Q", "public");
        System.out.println(middle);
        /**
         * 下面这个方法调用会编译报错。 
         * 编译器自动打包参数为一个Double和两个Integer对象,而后寻找这些类的公共超类。
         * 事实上Double类和Integer类的共同超类有两个:Number类和Comparator接口。
         * 因此这意味着ArrayAlg的这个方法返回值能够赋值给Number类型或Comparator类型。
         * 固然也能够将参数都改成double类型。(3.14,111d, 0d)
         */
        // double mid = ArrayAlg.getMiddle(3.14, 111, 0);
        // System.out.println(mid);


        LocalDate[] birthdays = { LocalDate.of(1906, 12, 9), // G. Hopper
                LocalDate.of(1815, 12, 10), // A. Lovelace
                LocalDate.of(1903, 12, 3), // J. von Neumann
                LocalDate.of(1910, 6, 22), // K. Zuse
        };
        Pair<LocalDate> mm = ArrayAlg.minmaxG(birthdays);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg {
    public static Pair<String> minmax(String[] a) {
        if (null == a || a.length == 0) {
            return null;
        }
        String min = a[0];
        String max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0)
                min = a[i];
            if (max.compareTo(a[i]) < 0)
                max = a[i];
        }
        return new Pair<String>(min, max);
    }

    /**
     * 泛型版本的 minmax 方法
     */
    public static <T extends Comparable> Pair<T> minmaxG(T[] a) {
        if (null == a || a.length == 0) {
            return null;
        }

        T min = a[0];
        T max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }

        return new Pair<T>(min, max);
    }

    // 泛型方法能够在普通类中定义,泛型参数放在修饰符和返回类型之间
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }

    /**
     * 类型变量的限定类型(bounding types,或译为界限类型)能够有多个,用&符号分隔:<T extend Comparable & Serializable>
     * 限定类型能够有多个接口,可是只能有一个类(类只能单继承),若是限定类型中有类,它必须是 限定列表中的第一个。
     */
    public static <T extends Comparable> T min(T[] a) {
        if (a == null || a.length == 0)
            return null;
        T smallest = a[0];
        for (int i = 1; i < a.length; i++)
            if (smallest.compareTo(a[i]) > 0)
                smallest = a[i];
        return smallest;
    }
}

interface A {
}

interface B {
}

// 接口能够继承接口,能够继承多个接口
interface C extends A, B {
}

class AA {
}

class BB {
}

// 类只能继承一个类,能够实现多个接口
class CC extends AA implements A, B {
}

 

3,

三,泛型 和 JVM(Java 虚拟机)

1,Java 虚拟机中没有泛型类型对象,全部的对象都属于普通类。

2,类型擦除

  • 当定义一个泛型类型(即定义一个泛型类)时,会自动定义提供一个相应的 原始类型(raw type)。原始类型的名字就是去除类型参数的后的泛型类型名。类中代码中的类型变量会被替换为限定类型列表中的第一个,若是没有限定类型就用Object替换。例如
    //例子一:原来的泛型类型
    public class Pair<T> {
         private T first;  
    }
    //类型擦除后为:
    public class Pair {
        private Object first;
    }
    
    //例子二:原来的泛型变量
    public class Pair<T extends Comparable & Serializable> {
        private T first;
    }
    // 类型擦除后为:
    public class Pair {
        private Comparable first;
    }
    // 注:Serializable 接口是标签接口(tagging interface:即没有任何方法的接口,只是用来instanceof用的),为了提升效率应将标签接口放在限定类型列表的末尾。
  • 编译器翻译泛型表达式:涉及到泛型的地方,编译器都会加入强制类型转换,好比:pair.getFirst()在编译器中返回一个Object对象,若是pair是Pair<Employee>的一个实例,那么就会将pair.getFirst()返回的Object对象强制转换为Employee对象。
  • 编译器翻译泛型方法【参考一篇好文章】:桥方法( bridge method )。注意:多态须要子类重写了父类的方法。

3,使用Java泛型时的约束与局限性: 大多数限制都是由类型擦除引发的。

  • 类型参数不能为基本类型(类型参数不能用基本类型实例化):如只有Pair<Double> 而没有Pair<double>。缘由是由于类型擦除。擦除以后,Pair类还有Object类型的域,而Object不能存储double值。注意:基本类型(primitive types)如int,double在Java中是独立的类型,不是继承自Object类,不属于Object。本篇开篇关于基本类型的说明中有详细解释。
  • 运行时类型检查只适用于原始类型。
    if (a instanceof Pair<String>) // Error
    if (a instanceof Pair<T>) //Error
    //强制类型转换编译器会警告
    Pair<String> p = (Pair<String>) a;
    
    // getClass()方法老是返回原始类型。
    Pair<String> stringPair = ...
    Pair<Employee> employeePair = ...
    if (stringPair.getClass() == employeePair.getClass()) // true:都返回Pair.class
  • 不能建立参数化类型的数组。
    //Error,不容许建立参数化类型的数组
    Pair<String>[] table = Pair<String>[][3];
    /* 注意:只是不能建立参数化类型的数组,可是声明类型为Pair<String>[]变量还是合法的,
        * 只是不能用new Pair<String>[3]初始化这个变量.
        * 能够声明通配类型的数组而后进行类型转换。可是结果是不安全的。
        */
    Pair<String>[] t = (Pair<String>[])new Pair<?>[3];
  • 可变参数的方法: 
    /**
     * 可变参数ts其实是一个数组。若是T是一个泛型类型,好比Pair<String>,
     * 那么Java虚拟机就必须创建一个Pair<String>数组,这就违反了不能建立泛型数组的规则。
     * 可是,对于这种状况,规则有所放松,只会有一个警告而不是错误。
     * 能够有两种方法抑制这个警告:
     * 方法一:为addAll方法增长注解@SuppressWarnings("unchecked")
     * 方法二:自Java SE7开始,还能够使用@SafeVarargs注解标注addAll方法。
     */
    // @SafeVarargs
    public static <T> void addAll(Collection<T> coll, T... ts) {
        for (T t: ts) {coll.add(t);}
    }
  • 不能实例化类型变量(类型变量):不能使用 new T(...), new T[...], T.class这样的表达式。
  • 不能构造泛型数组: T[] mm = new T[2] ;//Error
  • 静态域和静态方法 的类型不能为类型变量。 如: private static T first;//Error   public static T getFirst(){}// Error.
  • 不能抛出或捕获泛型类的实例。实际上,泛型类扩展Throwable都是不合法的。

4, 泛型类型的继承原则

  • 假设Manager是Employee的子类,则对于泛型来说有如下继承关系:①ArrayList<Employee> ② List<Employee> ③ ArrayList<Manager>④ List<Manager> ⑤ArrayList  ⑥List
    • ArrayList<Manager> 和 ArrayList<Employee>没有任何关系。List<Manager>和List<Employee>无任何关系。
    • ArrayList<Employee> 能够实现List<Employee>接口。ArrayList<Manager>能够实现List<Manager>接口。
    • ArrayList<Employee>  和 ArrayList<Manager>是 原始类型 ArrayList的子类。
    • List<Employee> 和 List<Manager>是 原始类型List的子类。
    • ArrayList(原始类型) 能够实现 List(原始类型)
  • Java中的泛型和数组之间的一个区别和联系:
    LocalDate date = LocalDate.of(1111, 11, 11);
    /*
        * Java 泛型
        */
    Manager m1 = new Manager("name", 3d, date);
    Manager m2 = new Manager("name1", 4d, date);
    Pair<Manager> managerPair = new Pair<>(m1, m2);
    
    //编译没法经过:Type mismatch: cannot convert from Pair<Manager> to Pair<Employee> Pair<Employee> employeePair = managerPair; /*
        * Java 数组
        */
    Manager[] managers = {m1, m2};
    Employee[] employees = managers;
    employees[0] = new Employee();//编译能够经过,可是运行时错误:java.lang.ArrayStoreException
    System.out.println(Arrays.toString(employees));

5,

四,泛型之 通配符类型 【点我参考另外一篇文章

1,通配符类型容许类型参数变化。注意:通配符不是在定义泛型类时使用的,而是在使用泛型类时才能用到通配符。如定义Pair类时,只能是public class Pair<T>{}不能是public class Pair<? extends Employee>

  • Pair<? extends Employee> 表示任何泛型Pair类型,它的类型参数是 Employee 的子类
  • 类型 Pair<Manager>是Pair<? extends Employee>的子类型。
  • Pair<? extends Employee> 类型是原始类型Pair的子类。

2,通配符捕获: 用于通配符不是一个类型,所以不能再编码中使用 ? 做为一种类型。假设,要交换Pair<?>的first和second属性,如下代码是错误的

? t = p.getFirst(); // Error
p.setFirst(p.getSecond());
p.setSecond(t);

解决方法是再写一个辅助方法swapHelper,以下

//  swapHelper是泛型方法
public static <T> void swapHelper(Pair<T> p) {
    T t = p.getFirst();
     p.setFirst(p.getSecond());
    p.setSecond(t);
}

/ **
   * swap方法能够调用泛型方法swapHelper,注意swap并非泛型方法,swap的参数是 * Pair<?>类型的。 swap方法中static和void之间并无泛型类型。 */
public static void swap(Pair<?> p) {
    swapHelper(p);
}

此时,swapHelper方法的参数T捕获了通配符。通配符捕获只有在许多有限制的状况下才是合法的。编译器必须可以确信通配符表达的是单个、肯定的类型。
例如:ArrayList
<Pair<T>>中的T永远不能捕获ArrayList<Pair<?>>中的通配符,由于数组列表ArrayList中能够保存两个?不一样的 Pair<?>。

3,

五,反射和泛型

1,

2,

 

 第九章:集合

一,Java集合框架概述

1,集合的接口与实现分离

※,接口与实现分离虚拟例子:队列接口:public interface Queue<E>{}  ,队列的实现方法一般有两种:

一是使用循环数组(circular array)public class CircularArrayQueue<E> implements Queue<E>{},

二是使用链表(linked list)。

※,

2,collection接口 和 Iterator 迭代器接口

※,Java类库中,集合的基本接口是Collection接口。Collection接口有两个基本方法:

public interface Collection<E> {
    boolean add(E element);
    Iterator<E> iterator();//迭代器
    ....//还有一些其余方法
}

※,Iterator接口包含4个方法:

public interface Iterator<E> {
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

※,for each循环是迭代器循环 while (iterator.hasNext()) {next = iterator.next();...}的简化 形式。 for each循环能够与任何实现了Iterable接口的对象一块儿工做。Iterable接口只包含一个抽象方法:

public interface Iterable<E> {
    Iterator<E> iterator();
}

Collection 接口扩展(继承)了Iterable接口,所以对于标准类库中的任何集合均可以使用 for each 循环。

※,在Java SE8中,甚至能够不用写循环。调用forEachRemaining方法并提供一个lamda表达式(它会处理一个元素). 例子以下:

    Collection<Integer> collection = new ArrayList();
    collection.add(3);
    collection.add(4);
    Iterator iterator = collection.iterator();

    List<Integer> list = new ArrayList<>();
    //e是Object类型
    iterator.forEachRemaining(e-> {
	 list.add((Integer) e + 3);
    });
    System.out.println(list); //[6, 7]

※, 迭代时 元素被访问的顺序取决于集合类型。

  • 若是对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1。
  • 若是对HashSet进行迭代,每一个元素将会按照某种随机的顺序出现。能够遍历到全部的元素,可是没法预知元素被访问的次序。

※,实际上,JDK1.2中出现的Iterator接口中的next() 和 hasNext()方法与 JDK1.0中的Enumeration接口中的nextElement()和 hasMoreElement() 方法的做用是同样的。Java类库的设计者本能够选择使用Enumeration接口,可是他们不喜欢这个冗长的名字,因而引入了具备较短方法名的新接口。

※,Iterator.next() 与 InputStream.read() 能够看做是等效的。

※,

3,泛型实用方法

4,集合框架中的接口

二,具体的集合

1,链表

2,数组列表

3,散列集

4,树集

5,队列与双端队列

6,优先级队列

三,映射

1,

2,

四,视图与包装器

1,

2,

五,算法

1,

2,

六,遗留的集合

1,

2,

七,

1,

2,

八,

1,

2,

相关文章
相关标签/搜索