一,java基本程序设计结构:
1,在网页中运行的 Java 程序称为 applet。 要使用 applet ,需要启用 Java 的 Web 浏览器执行字节码。
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不能用于存储基本类型的值(没有用自动装箱功能)。
4.1,float类型的有效位数(精度)为6~7位。double类型的有效位数为15位。
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,初始化数据域的顺序:
- 所有数据域被初始化为默认值(0、false或null)。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 执行构造器方法。
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生成的。
- 注释中如要添加等宽字体,不要使用
xxx
而要使用{@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方法,子类可以有一个同签名但是返回值类型不同的方法。
class Super { private void get() {} } class Sub extends Super { /** * 子类没有继承父类的私有get()方法 * 因此可以签名相同但是返回值类型不同 */ public String get() { return "到头来都是为他人作嫁衣裳"; } }
- 方法重写含义: 如果一个子类方法的 签名(名称和 参数个数、类型) 和 返回值 和父类的一个方法的签名和返回值相同(子类返回值也可以是父类返回值的子类型),那么子类中的这个方法重写了父类中的这个方法。只要满足条件无论加不加@Override注解都是方法重写。注意:重载(overload)要求是:方法名称相同,但是参数个数或类型不同,重载并不检查返回类型。
- 子类继承父类时,如果子类和父类方法签名相同(不考虑父类的private方法),那么返回值类型也要兼容。不允许子类方法的签名和父类相同,但是返回值类型不同。原因是如果允许这样,子类就会继承父类的那个方法,那么子类方法中就有两个方法签名相同但是返回值类型不同的方法。这就相当于重载时只有返回值类型不同的两个方法,是不被允许的。
- 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
- ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,如ArrayList
。 - ArrayList
staff = new ArrayList ();// 在Java SE7之后,可以省去右边的类型参数。即ArrayList staff = new ArrayList<>();编译器将检查变量staff的泛型类型,然后将这个类型放入右边的<>中 - 使用add()方法添加一个元素到数组列表中。staff.add(new Employee());
※,动态改变大小的原理
- 数组列表管理着对象引用(staff)的一个内部数组。如果调用add()方法时内部数组空间已经被用完了,数组列表就将自动创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
- 如果能够估计出数组可能存储的元素数量,可以在填充数组之前就确定数组列表的容量,有如下两个方法:
- 调用ensureCapacity()方法,staff.ensureCapacity(100);
- 将初始容量传递给ArrayList构造器。ArrayList
staff = new ArrayList<>(100); - 指定容量后,编译器将分配一个包含100个对象的内部数组。然后调用100次add()方法而不用重新分配空间。
-
分配数组列表,如下所示 new ArrayList
(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
----------
ArrayListstaff = 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
employees = new ArrayList<>(100);//容量100,但是大小此时还是0 employees.set(0, new Employee());// employees中尚未含有第0个元素,编译通过但运行时报错。 - 使用add()方法添加新元素而不要用set()方法。
- 可以使用toArray()方法将ArrayList类型转换为Array类型
ArrayList
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
※,自动装箱(autoboxing)与自动拆箱(unbox): 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{ 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
- 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
getDeclaringClass();//返回枚举常量所在枚举类的类对象。r.getDeclaringClass();// class com.learn.java.RGB
- String name();// 枚举常量名。r.name();// RED;
- static E[] values();//返回一个包含全部枚举常量的的数组。RGB[] values = RGB.values();//
static
> T valueOf(Class
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
※,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{ public int compareTo(T o); }
// 语言标准规定: x.compareTo(y)和y.compareTo(x)必定是相反的。如果一个抛异常,另一个也应该抛异常。所以涉及子类继承时,需要判断是否子类和超类可以比较分两种情况处理。 // 情况1,如果子类和超类无法对比,那么就加上getClass类型判断 class Employee implements Comparable{ 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 { 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类实现Comparable 接口的的compareTo(String anotherString)方法的方式是按字典顺序比较字符串。 - 假设现在希望按照字符串的长度来排序,则可以使用Arrays.sort()的另一个版本:Arrays.sort(Object[] a, Comparator c);传入一个数组和一个比较器作为参数,比较器是实现了Compartor
接口的对象。 String[] srr = new String[]{"helloWorld", "bcde","abc","fuck", "good", "fuckU"}; System.out.println(Arrays.toString(srr));//[helloWorld, bcde, abc, fuck, good, fuckU] Comparator
lengthComparator = new LengthComparator(); Arrays.sort(srr, lengthComparator); System.out.println(Arrays.toString(srr));//[helloWorld, fuckU, bcde, fuck, good, abc] class LengthComparator implements Comparator { @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
等接口的通常用途是确保一个类实现一个或一组特定的方法。但是标记接口中不包含任何方法,它唯一的作用就是允许在类型查询中使用 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
接口只有一个抽象方法,属于函数式接口(下面专门再讲了Comparator接口,里面实际有两个抽象方法,但是依然属于函数式接口,具体见下面再谈Comparator接口)。所以Arrays.sort(Object[] a, Comparator c)中的第二个参数除了可以使用一个Comparator 对象之外,还可以直接传入一个lambda表达式。上面已经这么使用过了。 - 实际上,在Java中,对lambda表达式所能做的也只是能将其转换为函数式接口。即只能用函数式接口类型接受一个lambda表达式。甚至都不能用Object来接受一个lambda表达式,因为Object不是一个函数式接口。
- java.util.function包中定义了很多非常通用的函数式接口。一个尤其有用的函数式接口是Predicate
接口。ArrayList类有一个removeIf()方法,它的参数就是一个Predicate 。 public interface Predicate
{ boolean test(T t); // additional default and static methods } ArrayList list = new ArrayList<>(); list.removeIf(e -> e == null);//将list中所有为null的元素删除掉。 等价于以下: list.removeIf(new RemoveCond()); class RemoveCond implements Predicate { @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
的一个实现,表示不考虑字母大小写对字符串数组排序 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
names = Arrays.asList("Liverpool", "Kloop", "Chamberlain", "Mane", "Salah", "Firmino"); Stream stream = names.stream().map(Employee::new);//map方法会为names中每个元素调用Employee(String name)构造器 List staff = stream.collect(Collectors.toList()); //可以使用数组类型建立构造器引用。例如int[]::new 是一个构造器引用,它有一个参数即数组的长度。等价于lambda表达式:x -> new int[x] Stream stream1 = names.stream().map(Employee::new); Object[] staff1 = stream1.toArray(); Stream 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的上限是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
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
1.6-
static
ServiceLoaderload(Classservice);//创建一个service loader,这个loader将会加载实现了给定接口的所有类。 - Iterator
iterator(); - Stream
> stream() 9;//JDK 9才有的方法。 - Optional
findFirst() 9;// JDK 9才有。
-
- java.util.ServiceLoader.Provider
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
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();// 构造器名是
; 静态的构造器名是 。无法区分同名的重载方法。 - 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,日志代理
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
package tong.huang.shan;
// Pair是一个泛型类,也可以说是一个泛型类型(Java中的类也表示一个类型)。在编译器中,这个泛型类被翻译为一个普通的Pair类。 public class Pair{ 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" }; Pairminmax = ArrayAlg.minmax(arr); System.out.println(minmax); String middle = ArrayAlg. 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 mm = ArrayAlg.minmaxG(birthdays); System.out.println("min = " + mm.getFirst()); System.out.println("max = " + mm.getSecond()); } } class ArrayAlg { public static Pair 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 (min, max); } /** * 泛型版本的 minmax 方法 */ public static Pair 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 (min, max); } // 泛型方法可以在普通类中定义,泛型参数放在修饰符和返回类型之间 public static T getMiddle(T... a) { return a[a.length / 2]; } /** * 类型变量的限定类型(bounding types,或译为界限类型)可以有多个,用&符号分隔: * 限定类型可以有多个接口,但是只能有一个类(类只能单继承),如果限定类型中有类,它必须是 限定列表中的第一个。 */ public staticT 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
{ private T first; } //类型擦除后为: public class Pair { private Object first; } //例子二:原来的泛型变量 public class Pair { private T first; } // 类型擦除后为: public class Pair { private Comparable first; } // 注:Serializable 接口是标签接口(tagging interface:即没有任何方法的接口,只是用来instanceof用的),为了提高效率应将标签接口放在限定类型列表的末尾。 - 编译器翻译泛型表达式:涉及到泛型的地方,编译器都会加入强制类型转换,比如:pair.getFirst()在编译器中返回一个Object对象,如果pair是Pair
的一个实例,那么就会将pair.getFirst()返回的Object对象强制转换为Employee对象。 - 编译器翻译泛型方法【参考一篇好文章】:桥方法( bridge method )。注意:多态需要子类重写了父类的方法。
3,使用Java泛型时的约束与局限性: 大多数限制都是由类型擦除引起的。
- 类型参数不能为基本类型(类型参数不能用基本类型实例化):如只有Pair
而没有Pair 。原因是因为类型擦除。擦除之后,Pair类还有Object类型的域,而Object不能存储double值。注意:基本类型(primitive types)如int,double在Java中是独立的类型,不是继承自Object类,不属于Object。本篇开篇关于基本类型的说明中有详细解释。 - 运行时类型检查只适用于原始类型。
if (a instanceof Pair
) // Error if (a instanceof Pair ) //Error //强制类型转换编译器会警告 Pair p = (Pair ) a; // getClass()方法总是返回原始类型。 Pair stringPair = ... Pair employeePair = ... if (stringPair.getClass() == employeePair.getClass()) // true:都返回Pair.class - 不能创建参数化类型的数组。
//Error,不允许创建参数化类型的数组 Pair
[] table = Pair [][3]; /* 注意:只是不能创建参数化类型的数组,但是声明类型为Pair []变量仍是合法的, * 只是不能用new Pair */ Pair[3]初始化这个变量. * 可以声明通配类型的数组然后进行类型转换。但是结果是不安全的。 [] t = (Pair [])new Pair>[3]; - 可变参数的方法:
/** * 可变参数ts实际上是一个数组。如果T是一个泛型类型,比如Pair
, * 那么Java虚拟机就必须建立一个Pair */ // @SafeVarargs public static数组,这就违反了不能创建泛型数组的规则。 * 但是,对于这种情况,规则有所放松,只会有一个警告而不是错误。 * 可以有两种方法抑制这个警告: * 方法一:为addAll方法增加注解@SuppressWarnings("unchecked") * 方法二:自Java SE7开始,还可以使用@SafeVarargs注解标注addAll方法。 void addAll(Collection 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
② List ③ ArrayList ④ List ⑤ArrayList ⑥List - ArrayList
和 ArrayList 没有任何关系。List 和List 无任何关系。 - ArrayList
可以实现List 接口。ArrayList 可以实现List 接口。 - ArrayList
和 ArrayList 是 原始类型 ArrayList的子类。 - List
和 List 是 原始类型List的子类。 - ArrayList(原始类型) 可以实现 List(原始类型)
- ArrayList
- Java中的泛型和数组之间的一个区别和联系:
LocalDate date = LocalDate.of(1111, 11, 11); /* * Java 泛型 */ Manager m1 = new Manager("name", 3d, date); Manager m2 = new Manager("name1", 4d, date); Pair
managerPair = new Pair<>(m1, m2); //编译无法通过:Type mismatch: cannot convert from Pair to Pair /* * Java 数组 */ Manager[] managers = {m1, m2}; Employee[] employees = managers; employees[0] = new Employee();//编译可以通过,但是运行时错误:java.lang.ArrayStoreException System.out.println(Arrays.toString(employees));Pair employeePair = managerPair;
5,
四,泛型之 通配符类型 【点我参考另一篇文章】
1,通配符类型允许类型参数变化。注意:通配符不是在定义泛型类时使用的,而是在使用泛型类时才能用到通配符。如定义Pair类时,只能是public class Pair
- 如 Pair extends Employee> 表示任何泛型Pair类型,它的类型参数是 Employee 的子类。
- 类型 Pair
是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 staticvoid swapHelper(Pair 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>中的T永远不能捕获ArrayList >中的通配符,因为数组列表ArrayList中可以保存两个?不同的 Pair>。
3,
五,反射和泛型
1,
2,
第九章:集合
一,Java集合框架概述
1,集合的接口与实现分离
※,接口与实现分离虚拟例子:队列接口:public interface Queue
一是使用循环数组(circular array)public class CircularArrayQueue
二是使用链表(linked list)。
※,
2,collection接口 和 Iterator 迭代器接口
※,Java类库中,集合的基本接口是Collection接口。Collection接口有两个基本方法:
public interface Collection{ boolean add(E element); Iterator iterator();//迭代器 ....//还有一些其他方法 }
※,Iterator接口包含4个方法:
public interface Iterator{ 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{ Iterator iterator(); }
Collection 接口扩展(继承)了Iterable接口,因此对于标准类库中的任何集合都可以使用 for each 循环。
※,在Java SE8中,甚至可以不用写循环。调用forEachRemaining方法并提供一个lamda表达式(它会处理一个元素). 例子如下:
Collectioncollection = new ArrayList(); collection.add(3); collection.add(4); Iterator iterator = collection.iterator(); List 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,