Skip to main content

Core Java Volume Ⅰ

v11

2 Java程序设计环境

2.1 JShell

$ jshell
jshell> "Core".length()
$1 ==> 4

jshell> 5*$1+2
$2 ==> 22
jshell> /exit
| 再见

tab补全 public 访问修饰符(access modifier) 驼峰命名法(camel case) main 方法必须是public,否则,可以通过编译,但是运行报错

错误: 在类 Welcome 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)

Java中的所有函数都是某个类的方法。 main方法必须是静态的。

3 基本程序设计结构

3.1 注释

有三种注释,

// 单行注释
/*
多行注释
*/
/**
* 文档注释:用来自动的生成文档。
*/

3.2 数据类型

8中基本类型,4种整型、2种浮点型、1种字符型、1种布尔型 Java数据类型所占据的字节数与平台无关。 Java没有无符号类型。 如果非得用正数,可以使用Byte.toUnsignedInt(b)

jshell> a = 0
a ==> 0

jshell> Byte.toUnsignedInt(a)
$6 ==> 0

jshell> a = -1
a ==> -1

jshell> Byte.toUnsignedInt(a)
$8 ==> 255

jshell> a = -2
a ==> -2

jshell> Byte.toUnsignedInt(a)
$10 ==> 254
if(x == Double.NaN) // 不能检测一个特定的值是否等于NaN
if(Double.isNaN(x)) // 可以用isNaN方法来判断

char类型的范围从\u0000到\uFFFF

Unicode**转义序列会在解析代码之前得到处理**。例如"\u0022+\u0022"并不是一个由引号包围加号构成的字符串,是加上会在解析之前转换为",得到""+"",也就是一个空串

// 当心注释中的\u。下面注释会产生语法错误
// \u000A is a newline

// 语法错误,因为\u后面没有跟着4个十六进制数
// look inside c:\users

char类型描述UTF-16编码中的一个代码单元,但是有些字符的编码为两个代码单元,所以不建议程序中使用char类型,除非确实需要处理UTF-16代码单元,最好将字符串作为抽象数据处理。

3.3 变量与常量

变量

变量名必须是以字母开头并由字母数字构成的序列。Java中的数字和字母的范围更大

// 判断哪些Unicode字符属于Java中的“字母”
// java中的内置方法,该方法确定指定的字符是否允许作为Java标识符中的第一个字符。
public static boolean Character.isJavaIdentifierStart(char ch)
//java中的一个内置方法,用于确定指定的字符是否可以作为第一个字符以外的Java标识符的一部分。
public static boolean Character.isJavaIdentifierPart(int codePoint)
public static boolean isJavaIdentifierPart(char ch)

尽管$是合法字符,不要用在自己的代码中,他只用在java编译器或其他工具生成的名字中。 Java9中 单下划线_ 不能作为变量名,将来可能用作通配符 可以在一行中声明多个变量,但是不提倡,逐一声明可以提高可读性。 许多程序员习惯将变量名命名为类型名

Box box;
Box aBox;

使用未初始化的变量会编译报错。 变量声明尽可能靠近第一次使用的地方,这是一种良好的程序编写风格。 从Java10开始,对于局部变量如果可以从变量的初始值推断出它的类型,就不需要使用声明类型

var days = 12;
var greeting = "Hello!";

常量

使用关键字final,习惯上常量名使用全大写。

final double WIDTH = 8.5;

类常量,方法外面。

public class Student{
public static final int COUNT = 8;
public static void main(String[] args){

}
}

枚举类型

enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE};
Size s = Size.MEDIUM;
// 枚举类型只能是枚举值或者null

3.4 运算符

整数被0除会产生一个异常,浮点数被0除会得到无穷大或NaN结果。 println不是静态方法,Math.sqrt是静态方法。 floorMod方法 计算一个时钟时针的位置。这里要做一个时间调整,而且要归一化为一个0-11之间的数。这很简单:(position + adjustment) % 12”。不过,如果这个调整为负会怎么样呢?你可能会得到一个负数。所以要引入一个分支,或者使用((position + adjustment) % 12+12)%12。不管怎样都很麻烦。 floorMod方法就让这个问题变得容易了:flooraMod(position + adjustment,12)总会得到一个0-11之间的数。 调整为正则顺时针转,调整为负逆时针旋转,需要注意的是如果除数为负,结果为负 参考文档 提示:不必在每个方法名前都加Math,可以静态引入

import static java.lang.Math.*

在Math类中,为了达到最佳的性能,所有方法都是用附件单元中的例程。如果得到一个完全可预测的结果比运行速度更重要的话,那么就应该使用StrictMath类。它实现了“可自由分发的数学库”的算法,确保在所有平台上得到的相同的结果。 Math提供了一些使函数使整数有更好的运算安全性。如果溢出,数学运算符只是悄悄的返回错误的结果不做任何提醒,例如Math.multiplyExact(100000000,0),就会产生一个异常,可以进行捕获或者让程序终止。还有一些方法(addExact、subtractExact、incrementExact、decrementExact、negateExact)

数值类型之间的转换

img

虚线有精度损失。

int、long转float可能有精度损失,long转double有可能精度损失 int转double无信息丢失。

强制类型转换

舍入运算用Math.round(),返回long类型,可以进行int强制类型转换。

结合赋值和运算符

如果运算符得到一个值,其类型与左侧的类型不同,就会发生强制类型转换。例如x是int类型,则一下语句

x += 3.5
// 只去整数部分

是合法的,将把x设置为(int)(x+3.5) 建议不要在表达式中使用++,因为这样的代码很容易让人困惑,而且会带来烦人的bug

关系和boolean运算符

&&和||短路效应 expression1&&expression2 若第一个为false,后面的就不用计算了 expression1||expression2 若第一个为true,后面的就不用计算了 三目运算符

位运算

求某一位或者将其他位掩掉只留下某一位。 位运算没有短路效应

>>>会用0填充高位
>>会用符号位填充高位
不存在<<<运算符
警告:对于int类型1<<35等同于1<<3
运算符优先级

从右向左:医院运算和强制类型转换、三目运算符、赋值运算符

3.5 字符串

Java字符串就是Unicode字符序列。

子串

String.substring(0,3); // 前三个字符即0 1 2

拼接

非字符串的值与字符串+,会转换成字符串。 String.join("Hello,", "world!");

Java 11中提供了repeat方法 String repeated = "Java".repeat(3);

不可变字符串

Java文档中将String类对象成为不可变的(immutable) greeting = greeting.substring(0,3) + "p!"; 拼接字符串的效率确实不高。但是有个优点是可以让字符串共享。 Java设计者认为共享带来的高效率远远胜过提取子串、拼接子串所带来的低效率。 (例外情况,对于单字符的操作,Java单独提供了类)

检测字符串是否相等

==是检测位置 "a".equals("a")是判断字符是否相等。

String greeting = "Hello";
if(greeting == "Hello") // 可能是true
if(greeting.subtring(0,3) == "Hel") // 可能是false

空串和null串

// 要检查一个字符串是否为null,要使用以下条件
if(str == null)
// 检查一个字符串既不是null,又不是空串
if(str != null && str.length != 0)

码点与代码单元

char数据类型是一个采用UTF-16编码标识Unicode码点的代码单元。常用字符使用个代码单元就可以,辅助字符需要一对代码单元表示。

// length方法返回代码单元的数量
String greeting = "Hello";
int n = greeting.length();
// 获取码点数量
int cpCount = greating.codePoint(0, greeting.length());
// 返回位置n的代码单元,n介于0~length()-1之间。
char c = greeting.charAt(n)
// 得到第i个码点
int index = greeting.offsetByCodePoints(0, i)
int cp = greeting.codePointAt(index);

emoji表情符号可能会在U+FFFF以上 codePoints方法可以生成一个int值的“流”,每个int值对应一个码点。

// 字符串转码点
int[] codePoints = str.codePoints().toArray();
// 码点转字符串
String str = new String(codePoints, 0, codePoints.length);

虚拟机不一定把字符串实现为代码单元序列。在java9中只包含单字节代码单元的字符串使用byte数组实现,所有其他字符串使用char数组。

StringAPI

// 按字典顺序比较
int compareTo(String other)
// 空或者由空格组成
boolean blank()
//
int indexOf(String str)
int lastIndexOf(String str)
String toLowerCase()
String toUpperCase()
String trim()
...

构建字符串

有些时候需要由较短的字符串构建字符串,例如,按键或来自文件中的单词。如果采用字符串拼接的方式效率会比较低。每次拼接字符串时,都会构建一个String对象,既耗时又浪费空间。使用StringBuilder可以避免这个问题的发生。

StringBuilder builder = new StringBuilder();
builder.append(ch);
builder.append(str);
String s = builder.toString();

StringBuilder在Java5中引入,他的前身是StringBuffer,效率较低,但允许采用多线程的方式添加或删除字符。如果所有字符串编辑操作大都在单个线程中执行(通常都这样),则应该使用StringBuilder。这两个类的API是一样的。

3.6 输入与输出

读取输入

Scanner in = new Scanner(System.in);
String name = in.nextLine();

nextLine是因为输入行中可能包含空格。想要读取一个单词,可以调用in.next() nextInt() nextDouble()

import java.util.*; Scanner类在util包中。 不在java.lang包中的类都要引入。

Console cons = System.console();
String username = cons.readLine("User name: ");
char[] passwd = cons.readPassword("Password: ");

Console没有Scanner方便,每次读取一行输入,没有能够读取单个单词或数值的方法。

格式化输出

System.out.printf("%8.2f", x); // 3333.33
System.out.printf("%,.2f", x); // 3,333.33
System.out.printf("%,(.2f", -3333.333); //(3,333.33)
格式含义
+打印正负号
0前面补0
%1$d %1$x以十进制和十六进制格式打印第一个参数
%d%<x以十进制和十六进制格式打印同一个数值
// 创建一个格式化的字符串而不打印输出
String message = String.format("name: %s, age:%d", name, age);

时间相关的方法

老的代码使用java.util.Date

新代码使用java.time包

System.out.printf("%tc", new Date());
// 周六 1月 13 18:10:47 CST 2024
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
// Due date: 一月 13, 2024
System.out.printf("%s %tB %<te, %<tY", "Due date:", new Date());

文件输入与输出

// 如果文件名中有反斜杠,则需要转义:c:\\mydirectory\\myfile.txt
Scanner in = new Scanner(Path.of("myfile.txt", StandardCharsets.UTF_8));
// 如果文件不存在,创建该文件
PrintWriter out = new PrintWriter("myfile.txt", StandardCharsets.UTF_8);
// 获取创建目录位置
String dir = System.getProperty("user.dir");
// 用一个不存在的文件构造一个Scanner或者用一个无法创建的文件名构造一个PrintWriter会产生异常
public static void main(String[] args) throws IOException {
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
}
java MyProg < myfile.txt > output.txt
# 关联到System.in 和System.out 就不必担心处理IOException异常了。

3.7 控制流程

块作用域

不能在嵌套的两个块中声明同名的变量。

条件语句

if (confition) statement1 else statement2

循环

while (condition) statement do statement while(condition);

确定循环

for (int i = 1; i <= 10; i++){
System.out.println(i);
}

计数器初始化、循环前检测循环条件、更新计数器 应该对同一个计数器变量进行初始化、检测和更新。否则常常晦涩难懂 检测的时候避免浮点数片段,避免double x = 0; x != 10; x += 0.1 for循环只不是是while循环的一种简化形式。

多重选择:switch语句

switch(choice) {
case 1:
...
break;
case 2:
...
break;
default:
...
break;
}

case标签可以使:

  • char、byte、short、int的常量表达式
  • 枚举常量
  • Java7开始case标签还可以是字符串字面量。

中断控制流程的语句

《Structured Programming with goto statements》,这篇文章中说,无限制地使用goto语句确实很容易导致错误,但在有些情况下,偶尔使用goto跳出循环还是有益处的。Java设计者同意这种看法,甚至在Java语言中增加了一条新的语句:带标签的break语句,以此来支持这种程序设计风格。

read_data:
while(...) {
...
break read_data;
...
}
label: {
...
if(confition) break label; // exits block
}

当然,并不提倡使用这种方式。另外需要注意,只能跳出语句块,而不能跳入语句块。 还有一种continue以及带标签的continue。

3.8 大数

对应与基本的整数和浮点数,java.math包中两个很有用的类:BigInteger和BigDecimal。这两个类可以处理包含任意长度序列的数值。

BigInteger a = BigInteger.valueOf(100);
BigInteger reallyBig = BigInteger.valueOf("99999999999999999999999999999999");

常量:BigInteger.ERO、BigInteger.ONE、BigInteger.TEN。Java9之后增加了BigInteger.TWO。 运算使用add、subtract、multiply、divide和mod。

BigInteger d = c.myltiply(b.add(BigInteger.valueOf(2)));

3.9 数组

数组存储相同类型值的序列。

声明数组

int[] a; // 声明数组 大多数喜欢这种风格
int a[]; // 声明数组
a = new int[100]; // 初始化数组
int[] a = new int[100]; // 声明并初始化
int[] a = {2, 3, 4, 5}; // 简写方式
String[] authors = {
"James Gosling",
"Bill Joy",
}; // 最后一个元素后面允许有逗号
a = new int[]{1, 2, 3}; // 匿名数组
a = new int[]{}; // 允许长度为0的数组。 长度为0的数组和null并不相同。

访问数组元素

for(int i = 0; i < a.length; i++)
System.out.println(a[i]);

for each循环

for(variable: collection) statement

collection这一集合表达式必须是一个数组或者一个实现了Iterable接口的类对象,例如ArrayList。 for each更加简洁、更不容易出错,因为不必为起始值和终止值操心。 适用于处理集合中的所有元素,如果只是处理部分元素还是需要使用传统的for循环。 快速打印数组中的所有值:Arrays.toString(s) // [1, 3, 4, 5]

数组拷贝

// 浅拷贝
int[] luckNumbers = smallPrimes;
luckNumber[5] = 12; // now smallPrimes[5] is also 12
// 深拷贝
int[] copiedLuckNumbers = Arrays.copyOf(luckNumbers, luckNumbers.length);
// 第二个参数是新数组的长度。这个方法通常用来增加数组的大小。
luckNumber = Arrays.copyOf(luckNumbers, 2 * luckNumbers.length);
// 未赋值的元素被赋值为默认值,数值型为0,布尔型为false。

命令行参数

java Message -g cruel world 
# Message后面的参数会传递到main函数的args数组中

数组排序

int[] a = new int[10000];
Arrays.sort(a); // 该方法使用了优化的快速排序算法

多维数组

double[][] balances;
balances = new double[NYEARS][NRATES];
int[][] magicSquare = {
{1, 2, 3},
{4, 5, 6}
}
for (double[] row : a)
for (double value : row)
do something with value
System.out.println(Arrays.deepToString(a));

不规则数组

多维数组理解为数组的数组。 不规则数组即数组的各个数组长度不规则。

4 对象与类

4.1 面向对象程序设计概述

OOP:object-oriented programming 传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程,就要考虑存储数据的适当方式。这就是Pascal语言的设计者将其著作命名为《Algorithm + Data Structures = Programs》。即算法第一位。 而对于OOP却调换了这个次序,将数据放在第一位,然后在考虑操作数据的算法。 小规模的问题分解为过程的开发方式比较理想。 大规模的问题适合采用面向对象的思想。

类构造对象的过程称为创建类的实例。 封装从形式上看,就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。 关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。 通过扩展一个类来建立另外一个类的过程称为继承。

对象

对象的三个特征:

  • 对象的行为
  • 对象的状态
  • 对象的标识——区分具有相同行为与状态的不同对象

改变对象的状态必须调用方法来实现,否则破坏了封装性。 关键特征会彼此影响,行为改变状态,状态影响它的行为。

识别类

一种经验:名词即类,动词即方法。

类之间的关系

  • 依赖(uses-a):一个类的方法使用或操纵另一个类的对象,就说一个类依赖于另一个类。尽可能减少耦合
  • 聚合(has-a):一个类的对象包含另一个类的对象;
  • 继承(is-a):一个类扩展另一个类

4.2 使用预定义类

对象与对象变量

// 对象传递给方法
System.out.println(new Date());
// 新构造的对象应用方法
String s = new Date().toString();
Date deadline = new Date(); // deadline 是对象变量,new Date()构造了一个对象

LocalDate类

Date类:用来标识时间点

LocalDate:日历表示法表示日期的。

LocalDate不需要构造器来构建,应当使用静态工厂方法,它会代表你调用构造器。

LocalDate.now();
LocalDate newYearsEve = LocalDate.of(2024, 1, 14);
newYearsEve.getYear();
newYearsEve.getMonthValue();
newYearsEve.getDayOfMonth();
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);

实际上,Date类也有得到年月日的方法,不过已经废弃,不鼓励使用。 提示:jdeprscan工具可以帮助检查代码中是否使用了已经废弃的特性。

jdeprscan [ options ]{dir|jar|class}

更改器方法与访问器方法

更改和访问属性的方法

4.3 用户自定义类

class ClassName {
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}

多个源文件的使用

对于Employee.java 和EmployeeTest.java两个文件的编译,有两种办法:

  1. javac Employee*.java
  2. javac EmployeeTest.java ## 检测到调用Employee.java ,也会进行编译

使用var声明局部变量

在Java10中,如果可以从变量的初始值推导出它们的类型,那么可以使用var关键字声明局部变量,而无需指定类型。

var harry = new Employee("A", 500, 1999, 10, 1);

这样可以避免重复写类型名Employee。 不过我们不会对数值类型使用var,如int、long或double,使你不用担心0、0L、0.0之间的区别。

使用null引用

对null值应用一个方法会产生一个NullPointerException异常。

if(n == null) name="unknown"; else name = n;
// java9
public Employee(String n) {
name = Objects.requireNonNullElse(n, "unknown");
}
public Employee(String n) {
Objects.requireNonNull(n, "The name cannot be null");
name = n;
}

隐式参数与显式参数

number007.raiseSalary(5)

raiseSalary有两个参数,第一个参数称为隐式(implicit)参数,是方法名前的Employee类型的对象。第二个参数位于方法名后面括号里的数值,是显式(explicit)参数。

封装的优点

访问器方法和更改器方法可能需要做很多工作。但是这将为我们带来的一个好处是:更改器方法可以完成错误检查,比如setSalary方法可以检查工资是否小于0。

警告:注意不要编写返回可变对象引用的访问器方法。

class Employee {
private Date hireDay;
public Date getHireDay() {
// 破坏了封装性
return hireDay; // Date d = harry.getHireDay()
}
public Date getHireDay() {
return (Date)hireDay.clone(); // OK
}
}

基于类的访问权限

class Employee {
public boolean equals(Employee other) {
// Employee类的方法可以访问任何Employee类型的私有变量
return name.equals(other.name);
}
}

私有方法

final实例字段

如果类中的所有方法都不会改变其对象,这样的类就是不可变的类。例如String类就是不可变的。 可变的类还使用final修饰可能会造成混乱。

private final StringBuilder evaluations;
public void giveGoldStar() {
evaluations.append(LocalDate.now()+": Gold star!\n");
}

4.4 静态字段和静态方法

class Employee {
private static int nextId = 1;
private int id;
}

每一个Employee对象都有一个自己的id字段,但是这个类的所有实例都将共享一个nextId字段。

nextId属于类,即使没有实例,也存在。

public void setId() {
id = nextId;
nextId++;
}

静态常量

静态变量使用的比较少,但静态常量却很常用。例如:

public class Math {
public static final double PI = 3.1415926;
}

public class System {
public static final PrintStream out = null;
}

这里System.out看到是final类型,并且默认值为null,可能会感到奇怪,为什么setOut方法可以修改他的值,原因在于,setOut方法是一个原生(native)方法,而不是在Java语言中实现的。原生方法可以绕过Java语言的访问控制机制。

静态方法

例如:Math.pow();

public static int getNextId() {
return nextId;
}

书上说这个方法也可以使用employee.getNextId()调用,实际测试不能用,只能Employee.getNextId()。不建议前者,会造成混淆,其原因是getNextId方法计算的结果与employee毫无关系。

工厂方法

LocalDate.now和LocalDate.of都是工厂方法

NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
NumberFormat percentInstance = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormat.format(x));
System.out.println(percentInstance.format(x));

为什么NumberFormat不用构造器完成这些操作呢?有两个原因

  • 无法命名构造器。这里希望有两个不同的名字得到货币实例和百分比实例。
  • 使用构造器时,无法改变所构造对象的类型。实际上这两个方法返回的是子类。

main方法

每个类中都可以有一个main方法。这是单元测试的一个技巧。

## 单独测试一个类
java Employee
## 启动程序则不会执行Employee中的main
java Application

4.5 方法参数

按值调用(call by value) 按引用调用(call by reference) Java程序设计语言总是采用按值调用。也就是方法得到的是所有参数值的一个副本;

  • 基本数据类型
  • 对象引用(按值传递:引用的副本,实际上引用的是同一个对象)

按引用调用的反例

public static void swap(Employee x, Employee y) {
Employee temp = x;
x = y;
y = temp;
}

方法结束后x,y就被丢了。

4.6 对象构造

重载

完整地描述一个方法,需要指定方法名以及参数类型。这叫做方法的签名(signature)。例如:

indexOf(int)
indexOf(int, int)

默认字段初始化

有些人认为依赖默认值的做法是一种不好的编程时间。影响可读性。

无参数的构造器

显式字段初始化

参数名

public Employee(String aName)
public Employee(String name)

调用另一个构造器

this相当于构造器。

public Employee(double s) {
this("Employee#" + nextId, s);
}

初始化块

// object initialization block
{
id = nextId;
}

这种机制不是必须的,也不常见,通常会把初始化代码放构造器中。

static {

}

对象析构与finalize方法

在析构器中,最常见的操作是回收分配给对象的存储空间。Java会自动垃圾回收,不需要也不支持析构器。 使用了内存之外的其他资源,例如文件资源则需要回收。

4.7 包

包名

使用包的主要原因是确保类名的唯一性。

类的导入

  • 使用 完全限定名
  • 使用import语句
星号只能导入一个包,而不能使用import java.*或 import java.*.*导入
import java.util.*;
import java.sql.*;
// 此时使用Date会报错,可以添加
import java.util.Date;

静态导入

import允许导入静态方法和静态字段,而不是类。

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

在包中增加类

package com.horstmann.corejava;
public class Employee {
...
}

包访问

default 可以被同一个包中的所有方法访问。

pubic class Window extends Container {
String warningString;
}

这里的warningString不是private变量,这意味着多有方法都可以访问该变量,因为只有Window使用这个变量,应该设置为私有变量才合适,后来JDK明确地禁止加在包名以“java.”开头的用户自定义的类!现在应当使用模块封装包。

类路径

类存储在文件系统的子目录中。类的路径必须与包名匹配。 也可以存放在jar(Java归档)文件中。 jar文件使用zip格式组织文件和子目录

设置类路径

java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg

classpath就是指定从哪里找可执行的类。

  • 基目录 /home/user/classdir
  • 当前目录(.)
  • JAR文件 /home/user/archives/archive.jar

4.8 JAR文件

创建JAR文件

jar options file1 file2 ...
jar cvf CalculatorClasses.jar *.class icon.gif

jar命令的所有选项类似于tar命令

清单文件

MANIFEST.MF 位于JAR文件的一个特殊的TETA-INF子目录中。最小清单文件及其简单

Manifest-Version: 1.0

复杂举例

Manifest-Version: 1.0
Specification-Title: Java Platform API Specification
Specification-Version: 17
Specification-Vendor: Oracle Corporation
Implementation-Title: Java Runtime Environment
Implementation-Version: 17.0.9
Implementation-Vendor: Oracle Corporation
Created-By: 16 (Oracle Corporation)

# 创建包含清单文件的jar文件
jar cfm Application.jar manifest.mf com/company/mypkg/*.class
# 追加
jar ufm Application.jar manifest-additions.mf

警告:清单文件最后一行必须以换行符结束。

可执行jar文件
jar cvfe Application.jar org.example.App ./org/example/*.class
java -jar Application.jar
多版本jar文件

Java9中引入了多版本的jar,其中可以包含面向不同Java版本的类文件。 为了保证向后兼容,额外的类文件放在了META-INF/versions目录中 image.png Java8 完全不知道META-INF/versions目录,他只会加载遗留的类。Java9读取这个jar文件时会使用新版本。 要增加不同版本的类文件,可以使用 --release标志。

jar uf MyProgram.jar --release 9 Application.class
关于命令行选项的说明

jdk一直以来都是单个短横线加多个字母选项的形式,如:

  • java -jar...
  • javac -Xlint:unchecked ...

jar是个例外,这个命令遵循经典的tar命令选项格式,而没有短横线; jar cvf... java9开始可以使用--version而不是-version,另外可以使用--class-path 而不是 -classpath。

# 带--和多字母的选项的参数用空格或者等号分割
javac --class-path /home/user/classdir...
javac --class-path=/home/user/classdir...

# 单字母选项的参数可以用空格分隔,或者直接跟在选项后面
javac -p moduledir...
javac -pmoduledir... # 这种不是个好主意

4.9 文档注释

javadoc,由源文件生成一个HTML文档。

类注释

方法注释

字段注释

通用注释

/**
* @since text
* @author name
* @version text
* @see
* @link
*
*/

包注释

  • package-info.java:以/*/界定的Javadoc注释,后面是一个package语句。
  • 提供一个名为package.html。会抽取之间的所有文本。

注释抽取

命令生成文档

4.10 类设计技巧

1.一定要保证数据私有。 这是最重要的;绝对不要破坏封装性。

2.一定要对数据初始化。 Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。

3.不要在类中使用过多的基本类型。

4.不是所有字段都需要单独的字段访问器和字段更改器。

5.分解有过多职责的类

6.类名和方法名要能够体现他们的职责。

7.优先使用不可变的类。 LocalDate类以及java.time包中的其他类是不可变的。更改的问题在多线程同时更改一个对象就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全的在多个线程间共享其对象。

5 继承

继承的基本思想是,可以基于已有的类创建新的类。继承已存在的类就是复用(继承)这些类的方法。

5.1 类、超类和子类

定义子类

extends

覆盖方法

子类调用父类的方法 super.getSalary()

子类构造器

一个对象变量可以指示多种实际类型的现象称为多态(polymorphism)。在运行时能够自动地选择适当的方法,称为动态绑定。

继承层次

继承链

多态

对象变量是多态的。一个Employee类型的变量既可以应用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象。反之则不行。

理解方法调用

1.编译器查看对象的声明类型和方法名。 有可能存在多个名为f单参数类型不一样的方法。一一列举所有方法和其超类中的方法。至此,编译器一直到所有可能被调用的候选方法。

2.编译器要确定方法调用中提供的参数类型。至此,编译器已经知道需要调用的方法的名字和参数类型。

3.静态绑定。如果是private方法、static方法、final方法或者构造器,name编译器将可以准确的知道应该调用哪个方法。

4.动态绑定。虚拟机必须调用与x所引用对象的实际类型对应的哪个方法。

每次调用方法都要完成这个搜索,时间开销相当大。因此虚拟机预先为每个类计算了一个方法表,其中列出了所有方法的签名和要调用的实际方法。这样以来调用的时候,直接查表即可。

1.首先虚拟机获取e的实际类型的方法表。

2.虚拟机查找定义了getSalary()签名的类。

3.虚拟机调用这个方法。

覆盖方法的时候子类方法不能低于超类方法的可见性。

阻止继承:final类和方法

// 不允许扩展这个类
public final class Executive extends Manager {
// 类是final,不影响字段,字段不是final,还可以改变
private int id;
// 子类不能覆盖这个方法 final类的所有方法自动地成为final方法
public final String getName() {
return name;
}
}

方法或类声明为final的主要原因是:确保他们不会在子类中改变语义。例如String类就是final类。

强制类型转换

超类赋值给子类即便是使用了强制类型转换,也会出现ClassCastException异常。 转换前应使用instanceof进行检查。

if(staff[1] instanceof Manager) {
boss = (Manager)staff[1];
}

一般情况下,最好尽量少用强制类型转换和instanceof运算符。

抽象类

public abstract class Person {
private String name
public abstract String getDescription();
public String getName() {
return name;
}
}

建议尽量将通用的字段和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中

new Person() // 不允许
Person p = new Student(); // 允许

var people = new Person[2];
people[0] = new Employee();
people[1] = new Student();
for(Person p : people)
System.out.println(p.getDescription());
// p永远不会引用Person对象,而是应用诸如Employee或Student这样的具体子类的对象。
// Person中不能省略getDescription的声明。否则不能调用。

受保护访问

子类不能访问父类的private的内容。 如果希望子类的方法访问超类的某个字段。需要将这些类方法或字段声明为受保护(protected)。 受保护的字段只能由同一个包中的类访问。 小结:

  • 仅对本类可见——private
  • 对外部完全可见——public
  • 对本包和所有子类(包括别的包的子类)可见——protected
  • 对本包可见——默认,不需要修饰。

protected和default对于同一个包,表现一致;对于不同的包

5.2 Object:所有类的超类

不需要显性声明extends Object

Object类型的变量

Object obj = new Employee("Harry");
// 要想进行操作,还需要强制类型转换
Employee e = (Employee)obj;

除了基本数据类型不是对象,其余都是对象,数组也是对象。

equals方法

未被重写前比较两个引用数据类型的对象引用是否相同。 重写后比较的是数值是否相等。 getClass返回一个对象所属的类。

public class Employee {
// Object的equals方法的参数是Object类型,如果这里的参数是Employee,则不是重写。
public boolean equels(Object other) {
if(this == other) return true;
if(other == null) return false;
if(getClass()!=other.getClass()) return false;
Employee o = (Employee)other;
return Objects.equals(name, o.name)
&& salary == o.salary
&& hireDay.equals(o.hireDay);
}
}

防备name或hireDay可能为null的情况,需要使用Objects.equals方法。

public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

子类定义equals方法时,首先调用超类的equals,如果检测失败,对象就不肯相等。如果相等再继续比较。

public class Manager extends Employee {
public boolean equals(Object otherObject) {
if(!super.equals(otherObject)) return false;
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}

相等测试与继承

关于是使用intanceof还是使用getClass,下面给出最佳实践:

  • 如果子类可以由自己的相等性概念,则对称性需求将强制使用getClass检测。
  • 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样就可以再不同子类的对象之间进行相等性比较。

在员工和经理的例子中,只要相应的字段相等,就认为两个对象相等。如果两个Manager对象的姓名、年龄以及薪水均相等,而奖金不相等,就认为它们是不同的,因此我们要使用getClass检测。 但如果假设使用员工的ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测,而且应该将Employee.equals声明为final

完美的equals方法建议

1.显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量。

2.检测this与otherObject是否相等

if(this == otherObject) return true;

这条语句只是优化。因为逐个对比字段开销大。

3.检测otherObject是否为null,如果为null,返回false。

if(otherObject == null) return false;

4.比较this与otherObject的类。如果equals的语义可以在子类中改变,就使用getClass检测

if(getClass() != otherObject.getClass()) return false;

如果所有的子类都有相同的相等性语义,可以使用instanceof检测:

if(!(otherObject instanceof ClassName)) return false;

5.otherObject强制转换为相应类类型的变量 ClassName other = (ClassName) otherObject

6.比较字段

return field1 == other.field1

&& Object.equals(field2, other.field2)

&& ...

hashCode()

Object的hashCode根据对象的存储地址得出的散列码。String的hashCode根据内容字符串导出的,因此相同的字符串有相同的散列码。 重写了equals方法就必须重写hashCode方法,因为相等的对象,hashCode一定相等。

hashCode方法应该返回一个整数(可以是负数)。

public int hashCode() {
return 7*name.hashCode()
+ 11*new Double(salary).hashCode();
}
// 优化后:(Objects.hashCode如果参数是null,会返回0)
public int hashCode() {
return 7*Objects.hashCode(name)
+ 11 * Double.hashCode(salary);
}
// 更好的办法
public int hashCode() {
return Objects.hash(name, salary);
}

toString()

public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
// 优化后: 子类也可以使用
public String toString() {
return getClass().getName()+"{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
public String toString() {
return super.toString()+"{" +
"bonus='" + bonus +
'}';
}
// org.example.Employee{name='e', id=1}
// 数组
Arrays.toString(luckyNumbers);
Arrays.deepToString(luckyNumbers);
// toString是非常有用的调试工具
Logger.global.info("Current position = " + position);

强烈建议给自定义的每一个类添加toString方法。这样做不仅自己受益匪浅,所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

5.3 泛型数组列表

ArrayList是一个有类型参数的泛型类。不需要指定数组的大小。

声明数组列表

ArrayList<Employee> staff = new ArrayList<Employee>();
ArrayList<Employee> staff = new ArrayList<>();
// 泛型类型为Employee
// JDK10中可以使用var以避免重复写类名。
var staff = new ArrayList<Employee>();
// 会生成ArrayList<Object>()
var staff = new ArrayList<>();

在Java的老版本中使用Vector类实现动态数组。不过ArrayList类更加高效,没有任何理由在使用Vector类。

staff.add(new Employee("Tony"));

如果内部数组已经满了,数组列表就会自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

如果已经知道可能存储的元素数量可以使用

// 100或者更多,ArrayList会优化
staff.ensureCapacity(100);
// 直接指定具体个数
ArrayList<Employee> staff = new ArrayList<>(100);

这样一来,前100次add调用不会带来开销很大的重新分配空间。

一旦确定数量保持恒定,不在发生变化,就可以调用trimToSize方法。垃圾回收会将多余的空间回收。但是当再次新增时就需要花时间再次移动存储块,所以应确认不会增加时调用trimToSize方法。

访问数组列表元素

staff.set(i, harry); // 空间大小应大于i时才能set否则使用add添加新元素,set相当于替换。
Employee e = staff.get(i); //

下面这个技巧可以一举两得,既可以灵活的扩展数组,又可以方便的访问数组元素。首先创建数组列表,并添加所有元素。然后使用toArray()方法将数组元素拷贝到一个数组中

var list = new ArrayList<X>();
while(...) {
x = ...;
list.add(x);
}
var a = new X[list.size()];
list.toArray(a);

// 删除元素
Employee e = staff.remove(n);
for(Employee e : staff) {
// do something whth e
}

类型化与原始数组列表的兼容性

为了看到警告文本信息,编译时提供 -Xlint:unchecked

数组列表应该加上类型避免不必要的结果。

5.4 对象包装器与自动装箱

基本类型转换为对象,例如Integer对应的基本类型int。这些类称为包装器(wrapper)。

数值型的公共超类Number。包装类不可变其中的值。包装类是final,不能派生子类。

new ArrayList<int>(); // 不允许
new ArrayList<Integer>(); // 允许

list.add(3);
list.add(Integer.of(3)); // 这种转变称为自动装箱 autoboxing

int n = list.get(i);
int n = list.get(i).intValue(); // 自动拆箱

// 自动装箱和拆箱也适用于算数表达式
Integer n = 3;
n++; // 编译器自动实现拆箱-计算-装箱

装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。 使用包装器还一个原因是可以封装方法。例如parseInt。

public int triple(Integer x) {// won't work  Integer是不可变的
...
}

import org.omg.CORBA.IntHolder;
public int triple(IntHolder x) {
x.value = 3*x.value;
return x.value;
}

5.5 参数数量可变的方法

public PrintStream printf(String format, Object ... args) {
return format(format, args); //arg 为数组
}
// 编译器自动装箱
System.out.printf("%d %s", new Object[]{new Integer(n), "widgets"});
// 示例 选出最大值
public static double max(double... values) {
double largest = Double.NEGATIVE_INFINITY;
for(double v : values) {
if(v > largest) {
largest = v;
}
}
return largest;
}

// 允许将数组作为最后一个参数传递给可变参数的方法。
System.out.printf("%d %s", new Object[]{new Integer(n), "widgets"});
public static void main(String... args);

5.6 枚举类

// 实际上,这个声明定义的类型是一个类,他刚好有3个实例,不可能构造新的对象
// 因此比较的时候不需要调用equals,直接使用==就可以
public enum Size { SMALL, MEDIUM, LARGE}
// 如果需要可以加构造器、方法、字段
public enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L");
private String abbreviation;
private Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}

枚举类型的构造器总是私有的。可以省略private修饰符。如果声明为public或protected会有语法错误。 所有有枚举类型都是Enum类的子类。继承了很多方法。

Size.SMALL.toString(); // "SMALL"
Size s = Enum.valueOf(Size.class, "SMALL");
Size[] values = size.values();
Size.MEDIUM.ordinal(); // 返回1,位置从0开始。

5.7 反射

反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序。使用反射,Java可以支持用户界面生成器、对象关系映射器以及其他需要动态查询类能力的开发工具。 能够分析类能力的程序称为反射(reflective)。主要是开发工具的程序员对他感兴趣,一般的应用程序员并不需要考虑反射机制。

Class类

程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。

Employee e;
Class cl = e.getClass();
Class cl = Class.forName(e.getClass().getName());

Class cl1 = Random.class;
Class cl2 = int.class;
Class cl3 = Double[].class;

无论在何时使用forName方法,都应该提供一个异常处理器。

警告:鉴于历史原因,getName方法对于数组会返回奇怪的名字 Double[].class.getName(); // "[Ljava.lang.Double;" int[].class.getName(); // "[I"

虚拟机为每个类型管理一个唯一的Class对象。

if(e.getClass() == Employee.class) // true

String className = "org.example.Employee";
Class cl = Class.forName(className);
// 如果这个类没有无参数的构造器,getConstructor方法会抛出一个异常。
Employee e = (Employee) cl.getConstructor().newInstance();

声明异常入门

异常有两种:

  • 非检查型异常(unchecked):下标越界
  • 检查型异常(checked):forName方法有throws编译器会提示

资源

类通常有一些关联的数据文件,这些文件被称为资源。 将文件与class文件一起放入JAR文件中是最方便的。

public class ResourceTest {
public static void main(String[] args) throws IOException {
Class cl = ResourceTest.class;
URL aboutURL = cl.getResource("about.gif");
var icon = new ImageIcon(aboutURL);

InputStream stream = cl.getResourceAsStream("data/about.txt");
var about = new String(stream.readAllBytes(), "UTF-8");
}
}

利用反射分析类的能力

检查类的结构 Field、Method和Constructor都有getName方法。

public static void main( String[] args ) throws Exception {
String className = "org.example.Employee";
Class cl = Class.forName(className);
for (Method method : cl.getMethods()) {
System.out.println(method.getName());
for (Parameter parameter : method.getParameters()) {
System.out.println(parameter.getName());
}
}
for (Field f: cl.getDeclaredFields()) {
System.out.println(f.getName());
}
}

使用反射在运行时分析对象

Employee harry = new Employee();
harry.setName("Harry");
Class cl = harry.getClass();
Field f = cl.getDeclaredField("name");
// 反射机制默认行为受限于Java的访问控制。可以通过setAccessible覆盖访问控制
f.setAccessible(true);
String name = (String) f.get(harry);
System.out.println(name);

AccessibleObject是Field、Method和Constructor的公共超类。 如果不允许访问,setAccessible调用会抛出一个异常。访问可以被模块系统或安全管理器拒绝。

将来的库有可能使用可变句柄而不是反射来读写字段。VarHandle和Field类似。可以用它读写一个特定类任意实例的特定字段。

使用反射编写泛型数组代码

public static Object goodCopyOf(Object a, int newLength) {
Class cl = a.getClass();
if (!cl.isArray()) return null;
Class componetType = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componetType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}

int a = {1, 2, 3, 4};
a = (int[]) goodCopyOf(a, 10);

调用任意方法和构造器

Java的设计者曾说过:方法指针是很危险的,而且容易出错。他们认为Java的接口和lambda表达式是一种更好的解决方案。不过反射机制允许你调用任意的方法。

Object invoke(Object obj, Object... args)
// 第一个参数是隐式参数,其余的对象提供了显式参数。
// 对于静态方法,第一个参数可以忽略,即设置为null。
// 假设m1标识Employee的getName方法
String n = (String)m1.invoke(harry);

// 注意同名方法,参数不同
Method getName = Employee.class.getMethod("getName");
Method raiseSalary = Employee.class.getMethod("raiseSalary", double.class);

// 注意构造器的参数
Class cl = Random.class;
Constructor cons = cl.getConstructor(long.class);
Object o = cons.newInstance(42L);

// 调用方法注意参数
Method method = Math.class.getMethod("sqrt", double.class);
double d = (Double) method.invoke(null, 9);

invoke容易出错抛出异常,另外invoke的参数和返回值必须是Object类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正错误会麻烦的多。而且代码要比直接调用的代码慢得多。 鉴于此,建议仅在绝对便要的时候才使用Method对象。通常更好的做法是使用接口以及Java8引入的lambda表达式。特别强调:建议不要使用回调的Method对象。可以使用回调的接口,这样不仅代码执行速度快,也更便于维护。

5.8 继承的设计技巧

1.将公共操作和字段放在超类中。

哪怕是声明,上面有提及。

2.不要使用受保护的字段。

3.使用继承实现“is-a”关系。

4.除非所有继承的方法都有意义,否则不要使用继承。

5.覆盖方法时,不要改变预期的行为。

替换原则(用来判断是否应将数据设计为继承关系):程序中出现的超类对象的任何地方都应该可以使用子类对象替换。

归根结底,关键在于在子类中覆盖方法时,不要偏离最初的设计想法。

6.使用多态,而不是使用类型信息。

if (x is of type1)
action1(x);
else if (x is of type2)
action2(x);
// 应该考虑多态。
x.action();

7.不要滥用反射。应用程序中不要使用。

6 接口、lambda表达式与内部类

接口用来描述类应该做什么,而不指定他们具体应该怎么做。

内部类技术在设计具有相互协作关系的类集合是很有用。

代理是一种实现任意接口的对象。

6.1 接口

接口的概念

接口不是类,而是对希望符合这个接口的类的一组需求。

public interface Comparable {
int compareTo(Object other);
}
// java5中,Comparable接口已经提升为一个泛型类型
public interface Comparable<T> {
int compareTo(T other);
}
// 仍然可以使用不带类型参数的原始类型

接口中的所有方法都自动是public方法。因此不必提供public。

阿里开发手册推荐不写public。

接口中还可以定义常量。接口中不会有实例字段。可以将接口看成是没有实例字段的抽象类。但是还有区别。

public class Employee implements Comparable{
public int compareTo(Object o) {
Employee other = (Employee) o;
// 前面比后面小返回一个负值;二者相等返回0;否则返回一个正值
return Double.compare(salary, other.salary);
}
}
// 使用泛型 避免对Object类型转换。
public class Employee implements Comparable<Employee>{
public int compareTo(Employee o) {
return Double.compare(salary, other.salary);
}
}

如果对员工编号排序,可以返回id-other.id,但要注意整数的范围不能过大,以免溢出,否则使用Integer.compare(x,y)

Comparable接口的文档建议compareTo方法应当与equals方法兼容。就是说x.equals(y)时x.compareTo(y)就应当等于0。大多数JavaAPI都遵循了这个建议。有一个例外是BigDecimal。BigDecimal("1.0")和BigDecimal("1.00")。equals为false,因为精度不同。compareTo为0。

为什么不能在Employee类中直接提供compareTo方法,而必须实现Comparable接口呢? 主要原因在于,java是强类型的语言。在调用方法的时候,编译器能检查这个方法确实存在。在sort方法中可能有这样的语句

if (a[i].compareTo(a[j])>0){
...
}

编译器必须确认a[i]一定有一个compareTo方法。 你可能认为sort方法接受的是Comparable[]数组,实际上,接受一个Object[]数组,并使用强制类型转换。

if ((Comparable)a[i].compareTo(a[j])>0){
...
}

与equals方法一样,在继承中可能出现问题。因为Manager扩展了Employee,而Employee实现的是Comparable<Employee>,而不是Comparable<Manager>。

补救方式有两种: 如果不同子类中的比较有不同的含义,就应该将不同类型之间的比较视为非法。

if(getClass() != other.getClass()) throw new ClassCastException();

如果存在能比较子类对象的通用算法,name可以在超类中提供一个compareTo方法,并将这个方法声明为final。

例如在Employee类中提供一个rank方法。让每个子类覆盖rank,并实现一个考虑rank值的compareTo方法。

接口的属性

接口不能用new实例化,却能声明接口的变量,接口变量必须引用实现了这个接口的类对象。

Comparable x = new Employee();
// 检查是否实现了接口
if (anObject instanceof Comparable)
// 接口继承
public interface Powered extends Moveable{
double milesPerGallon();
double SPEED_LIMIT = 95; // public static final constant
}

Java语言规范建议不要提供多余的关键字。

有些接口只定义常量,而没有定义方法,例如SwingConstants,不过这样的使用接口更像是退化,建议最好不要这样使用。

// 可以同时实现多个接口,逗号隔开
class Employee implements Cloneable, Comparable

接口与抽象类

有了抽象类为什么还需要接口呢?因为不能多继承。Java设计者选择了不支持多重继承,主要原因是多重继承会让语言变得非常复杂(C++),或者效率会降低(Eiffel)。

静态和私有方法

在Java8中允许在接口中增加静态方法。

目前为止,通常的做法是将静态方法放到伴随类中。如Collection/Collections或Path/Paths。在Java11中提供了等价的方法。

// Path是接口 Paths是实现了Path的类
Paths.get("jdk8", "conf", "security");
Path.of("jdk11", "conf", "security");
// 这样一来,Paths类就不再是必要的了

类似地,实现你自己的接口时,没有理由再为实现工具方法另外提供一个伴随类。

在Java9中允许private方法。private方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以用法很有限,只能作为接口中其他方法的辅助方法。

默认方法

可以为接口方法提供一个默认实现,必须用default修饰符。

public interface Comparable<T> {
default int compareTo(T other) {
return 0;
}
}

当然这并没有他大用处,因为每个具体实现都会覆盖这个方法。不过有些情况下可能有很用。例如Iterator接口,用于访问一个数据结构中的元素。这个接口生命了一个remove方法,

public interface Iterator<E> {
boolean hasNext();
E next();
default void remove(){
throw new UnsupportedOperationException("remove");
}
}

如果你要实现一个迭代器,就需要提供hasNext和next方法,因为没有默认实现,而如果你的迭代器是只读的,就不用操心remove方法。 默认方法可以调用其他方法。例如,Collection接口可以定义一个便利方法

public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0;
}
}

这样程序员就不用操心实现isEmpty方法了。

Java中Collection接口并没有这样做,实际上有一个AbstractCollection类实现了Collection,并根据size定义了isEmpty。建议程序员扩展AbstractCollection。不过那个技术已经过时,现在可以直接在接口中实现方法。

默认方法的一个重要用法是“接口演化”(interface evolution)新接口中增加了新方法,如果这个方法没有默认实现,老的类更新后不能保证“源代码兼容”。

解决默认方法冲突

超类和接口、接口和接口之间有同名参数相同的方法。

  1. 超类优先

如果超类提供了一个具体方法,同名而且有相同类型的默认方法会被忽略。超类优先,接口无论有没有默认方法都会被忽略。

  1. 接口冲突

如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须要覆盖这个方法来解决冲突。

class Student implements Person, Named {
public String getName() {
return Person.super.getName();
}
}

如果一个接口有默认实现,另一个没有默认实现,Java设计者更强调一致性。两个接口如何冲突并不重要。如果至少有一个提供了实现,编译器就会报告错误,程序员就必须解决这个二义性。 若两个都没实现,这以前一样,需要重写。 类优先规则可以确保与Java7的兼容性。如果为一个接口增加默认方法,这对于有这些默认方法之前能正常工作的代码不会有任何影响。

千万不要让一个默认方法重新定义Object类中的方法,例如toString或equals方法,尽管这对List之类的接口有吸引力,由于类优先的规则,这样的方法无法超越Object.toString 或 Objects.equals。

接口与回调

回调是一种常见的设计模式,可以指定某个特定事件发生时应该采取的动作。例如按下鼠标你可能希望完成某个特定的动作。 在java.swing包中有一个Timer类,如果希望经过一定时间间隔就得到通知,Timer类就很有用。 构造定时器时,需要设置一个时间间隔,并告诉定时器经过这个时间间隔时需要做些什么。 别的语言中传递的是方法,Java中传递的是对象。定时器要求传递对象实现了java.awt.event包中的ActionListener接口。

public interface ActionListener {
// 定时器会执行actionPerformed方法。
void actionPerformed(ActionEvent event);
}
class TimePrinter implements ActionListener {
// ActionEvent 提供了事件的相关信息,例如事件
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
// 响铃
Toolkit.getDefaultToolkit().beep();
}
}
var listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();

Comparator接口

前面已经了解实现了Comparable接口的实例,就可以排序,例如String实现了接口,并且String.compareTo方法按字典顺序比较字符串。 现在假设需要根据字符串的长度排序,Arrays.sort还有第二个版本,参数为一个数组和一个比较器(实现了Comparator接口的实例)。

// 接口定义
public interface Comparator<T> {
int compare(T first, T second);
}

public class LengthComparator implements Comparator<String> {
@Override
public int compare(String first, String second) {
return first.length() - second.length();
}
}
String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());

对象克隆

// 拷贝,还是同一个对象
Employee original = new Employee();
Employee copy = original;

// 克隆
Employee copy = original.clone();

不过没那么简单,clone方法是Object的一个protected方法,这说明你的方法不能直接调用这个方法,只有Employee类可以克隆Employee对象。这个限制是有原因的,Object对这个对象一无所知,只能逐个子弹进行拷贝。如果对象中的所有字段都是数值或其他基本类型,拷贝没有问题,但如果是对象包含子对象的引用,浅拷贝字段就会得到相同对象的另一个引用,这样一来,原对象和克隆的独享仍然会共享一些信息。 拷贝和克隆.jpg 默认的clone操作是浅拷贝。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,或者在对象的生命周期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况是安全的。 下图为浅拷贝浅拷贝.png 不过,通常子对象都是可变的,必须重新定义clone方法建立一个深拷贝,同时克隆所有子对象。在这个例子中hireDay是一个Date,这是可变的,可以也必须克隆。(如果是LocalDate是不可变的,就不需要做任何处理了)。(Date创建后还可以修改对象的值,LocalDate一旦创建就不可更改)。 因为Object类中的clone方法声明为protected,所以你的代码不能直接调anObject.clone()。子类只能调用受保护的clone方法来克隆他自己的对象。必须重新定义clone为public才能允许所有方法克隆对象。 Cloneable接口没有clone方法,接口知识作为一个标记,指示类设计者了解克隆过程。如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。 Cloneable是少数标机接口之一。也有称之为记号接口。唯一左右就是允许在类型查询中使用instanceof。 即使clone的默认(浅拷贝)实现能够满足要求,还是要实现一下接口。浅拷贝要做的只是把方法暴露出来

// 浅拷贝
class Employee implements Cloneable {
// 声明为public
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}
// 深拷贝
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date)hireDay.clone();
return cloned;
}

如果在一个对象上调用clone,但是这个对象没有实现Cloneable接口,Object类的clone方法会抛出一个CloneNotSupportedException。当然Employee和Date类实现了Cloneable接口。不过,编译器并不了解这一点,因此要声明这个异常。 public Employee clone() throws CloneNotSupportedException 除非是final类,否则最好还是保留throws,这样允许子类在不支持克隆时选择抛出一个CloneNotSupportedException。 必须当心子类的克隆,例如,以单位Employee类定义了clone方法,任何人都可用他来克隆Manager对象。Manager能不能克隆取决于它的子对象中有没有可变的。 要不要在自己的类中实现clone呢?承认clone相当笨拙,不过别的方法实现也会有同样的问题。毕竟不常用,标准库中只有不到5%的类实现了clone。

// 所有数组类型都有一个公共的clone方法,而不是受保护的。
int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
int[] cloned = luckyNumbers.clone();

克隆对象还一种机制,使用了Java的对象串行化特性。这个机制很容易实现,而且很安全,但效率不高。

6.2 lambda表达式

了解如何使用lambda表达式采用一种简洁的语法定义代码块,以及如何编写处理lambda表达式的代码。

为什么要引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。 像ActionListener的actionPerformed方法和Comparator的compare方法,重点都是代码块,在Java中传递一个代码块并不容易,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。 在其他语言中,可以直接处理代码块。Java设计者很长时间以来一直拒绝增加着特性。毕竟Java的强大之处就在于其简单性和一致性。倘若只要一个特性就能够让代码稍简洁一些,就把这个特性增加到语言中,很快就会变得一团糟。在Java中,也可以编写类似的API利用类对象实现特定的功能,不过这种API使用可能很不方便。 就现在来说,问题已经不是是否增强Java来支持函数式编程,而是要如何做到这一点。设计者们做了多年的尝试,终于找到一种适合Java的设计。

lambda表达式的语法

(String first, String second) -> {
if (first.length() < second.length()) {
return -1;
} else if (first.length() > second.length()) {
return 1;
} else {
return 0;
}
}
// 如果可以推导出参数类型,则可以忽略其类型
Comparator<String> comp
= (first, second)
-> first.length() - second.length();
// 编译器会推导出first和second必然是字符串。

// 单个参数可以省略小括号
ActionListener listener = event ->
System.out.println("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
// 不允许只在单个分支中使用 下面写法不合法。
(int x) -> {if(x>=0) return 1;}

函数式接口

Java中有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。 对于只有一个抽象方法的接口,称其为函数式接口(functional interface)。

为什么函数式接口必须有一个抽象方法。不是接口中的所有方法都是抽象的吗?实际上,接口完全有可能重新声明Object类的方法,如toString或clone,这些声明有可能会让方法不再是抽象的。更重要的是接口可以声明非抽象方法(static或default)。

虽然传递给sort的参数是lambda表达式,在底层,方法会接收实现了Comparator<String>的某个类的对象。在这个对象调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与传统的内联类相比,这样高效的多。 实际上lambda表达式所能做的也只是转换为函数式接口。lambda表达式不能赋值给Object的变量,Object不是函数式接口。 JavaAPI在java.util.function包中定义了很多非常通用的函数式接口。

// 定义
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
// 用法
BiFunction<String, String, Integer> comp
= (first, second) -> first.length() - second.length();
Integer n = comp.apply("aaa", "bb");

// 定义
public interface Predicate {
boolean test(T t);
}
// ArrayList的removeIf方法的参数就是一个Predicate。
list.removeIf(e -> e == null);

// 定义
public interface Supplier<T> {
T get();
}
// 供应者(supplier)用于实现懒计算
LocalDate hireDay = Objects.requireNonNullOrElse(day,
new LocalDate(1970, 1, 1));
// 上面这种带new的上来就初始化了
// 下面lambda表达式,用的时候调用才初始化
LocalDate hireDay = Objects.requireNonNullOrElse(day,
()-> new LocalDate(1970, 1, 1));
// requireNonNullOrElse方法只在需要值时才调用供应者。

懒计算

方法引用

var timer = new Timer(1000, System.out::println);
// 相当于
var timer = new Timer(1000, event-> System.out.println(event));

表达式System.out::println是一个方法引用。 编译器会从重载的println方法中选出println(Object x)方法。

Runnable task = System.out::println;
void run()
// 会选择无参的println();打印空行。

// 排序不考虑大小写
Arrays.sort(strings, String::compareToIgnoreCase);
  • object::instanceMethod System.out::println
  • Class::instanceMethod String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y)
  • Class::staticMethod Math::pow (x, y) -> Math.pow(x, y)

方法引用.png 又是API包含一些专门用作方法引用的方法。例如Objects类的isNull,用于测试一个对象引用是否为null。乍一看上去没有什么用,因为obj == null 比Objects.isNull(obj)更有可读性。不过可以把方法引用传递到任何有Predicate参数的方法。例如

list.removeIf(Objects::isNull);
// 比list.removeIf(e -> e == null); 可读性更高。

包含对象的方法引用与等价的lambda表达式还有一个细微的差别,考虑到方法引用separator::equals。如果separator为null,构造时会抛出空指针异常,lambda表达式,调用时才会抛出异常。 方法引用可以使用this 和 super

构造器引用

ArrayList<String> names = {...};
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collector.toList());

可以用数组类型建立构造器引用。例如int[]::new是一个构造器引用,相当于x -> new int[x] Java有一个限制,无法构造泛型类型T的数组。数组构造器对于克服这个限制很有用,new T[n]会产生错误,因为这会改为new Object[n]。例如: Object[] people = stream.toArray(); 这并不能让人满意。流库利用构造器引用解决了这个问题。

Person[] people = stream.toArray(Person::new);

变量作用域

public static void repeatMessage(String text, int delay) {
ActionListener listener = e -> {
System.out.println(text);
};
new Timer(delay, listener).start();
}

Manager.repeatMessage("hello", 1000);

这里有个问题,lambda标识可能在repeatMessage调用返回很久后才运行,而那时这个text参数变量已经不存在了。 lambda表达式的三部分:

  • 一个代码块
  • 参数
  • 自由变量的值,这是指非参数而且不在代码中定义的变量。(例如上面的text)

关于代码块以及自由变量有个术语叫闭包。java的lambda表达式就是闭包。

闭包扩展:在 JavaScript 中,闭包是指一个函数能够访问在它外部定义的变量。这些变量通常被称为“自由变量”,因为它们不是该函数的局部变量,也不是该函数的参数。闭包可以在函数内部创建,也可以在函数外部创建。

只能引用值不会改变的变量。下面的做法是不合法的。这个限制是有原因的。如果更改变量,并发执行多个动作就会不安全。

public static void repeatMessage(int start, int delay) {
ActionListener listener = e -> {
start++; // Error
System.out.println(text);
};
new Timer(delay, listener).start();
}

如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。例如:

public static void repeatMessage(String text, int count) {
for (int i = 1; i<= count; i++) {
ActionListener listener = e -> {
System.out.println(i + ":" + text); // Error
};
new Timer(1000, listener).start();
}
}

这里有条规则:lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final)即这个变量初始化后就不会在为它赋新值。这里的text总是指示同一个String对象是合法的。i的值会改变因此不能捕获i。 lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。

public static void repeatMessage(String text) {
BiFunction<String, String, Integer> comp
= (text, second) -> { // 外面有了text里面就不能再有
text.length() + second.length();
}
Integer n = comp.apply("a", "bb");
System.out.println(n);
}
  • 在一个方法中,不能有两个同名的局部变量,在lambda表达式中也不能有。
  • 在lambda表达式适用this就是创建这个lambda表达式的方法的this参数。
public class Application {
public void init() {
ActionListener listener = event -> {
System.out.println(this.toString());
// this指的是Application,而不是ActionListener。
}
}
}

处理lambda表达式

使用lambda表达式的重点是延迟执行,否则没必要使用。之所以在以后执行,有很多原因:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码:例如排序中的比较
  • 发生某种情况时执行代码:如点击了一个按钮,数据到达。
  • 只在必要时才运行代码。

示例:

public static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) {
action.run();
}
}
repeat(10, () -> System.out.println("Hello!"));

C0B8C16B320A16246EAFEDC7F06F8519.png

public interface IntConsumer {
void accept(int value);
}
public static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) {
action.accept(i);
}
}
repeat(10, (i) -> System.out.println("Countdown:" + (9 - i)));

37C0A95023E2E3BB2CA122579EA81F9C.png 上面特殊化接口比通用接口更高效,因此上面示例用的是IntConsumer而不是Consumer<Integer>。 最好使用上面两张表中的接口。对此有个遗留接口java.io.FileFilter,最好是使用标准的Predicate<File>。 大多数标准函数式接口都提供了非抽象方法来生成或合并函数,例如,Predicate.isEqual(a)等同于a::equals。已经提供了默认方法and、or和negate来合并谓词。例如:Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于 x -> a.equals(x) || b.equals(x) 自己设计的接口中 @FunctionalInterface 注解的优势可以提醒你过多的抽象方法及接口文档便利。并不一定非得加这个注解,但加上是个好主意。

再谈Comparator

Comparator有很多静态方法创建比较器。静态comparing方法取一个“键提取器”函数,他将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。

Arrays.sort(people, Comparator.comparing(Person::getName));

与手动实现一个Comparator相比,这当然要容易得多。另外代码也更清晰。

// 如果两个人的姓相同就使用第二个比较器。
Arrays.sort(people,
Comparator.comparing(Person::getLastname)
.thenComparing(Person::getFirstName));
// 变体形式 为提取的键指定比较器。例如根据人名长度完成排序
Arrays.sort(people, Comparator.comparing(Person::getName,
(s, t) -> Integer.compare(s.length(), t.length())));
// 避免int、long和double的装箱
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

如果键函数可以返回null,可能用到nullsFirst和nullsLast适配器,这些静态方法会修改现有的比较器,从而在遇到null时不会抛出异常。

Arrays.sort(managers, 
Comparator.comparing(Manager::getName,
Comparator.nullsFirst(Comparator.naturalOrder())));

reverseOrder提供了逆序。等同于naturalOrder().reversed()

6.3 内部类

  • 内部类可以对同一个包中的其他类隐藏。
  • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。

内部类原先对简洁的实现回调非常重要,不过现在lambda可以做的更好。但内部类对于构建代码还是很有用的。

使用内部类访问对象状态

内部类的语法相当复杂。鉴于此,我们选择一个简单但不太实用的例子来说明。

public class TalkingClock {
private int interval;
private boolean beep;

public TalkingClock(int interval, boolean beep) {
this.interval = interval;
this.beep = beep;
}

public void start() {
TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
public class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if(beep) {
System.out.println(e.getWhen());
}
}
}
}

TalkingClock clock = new TalkingClock(1000, true);
clock.start();

需要注意的是TimePrinter位于内部。这并不意味着每个TalkingClock都有一个TimePrinter实例字段。可以看到一个内部类方法可以访问自身的数据字段,也可以访问创建他自身的外围类对象的数据字段。 0C9090DAEE99F5CE68A20AF4A9F8105C.png 内部类的对象总有一个隐式引用,指向创建他的外部类对象。

public class TalkingClock {
...
public void start() {
TimePrinter listener = new TimePrinter(this);// 编译器会自动传入参数
Timer timer = new Timer(interval, listener);
timer.start();
}
...
public class TimePrinter implements ActionListener {
// 实现机制是增加构造方法,这里只是演示,编译器会自动生成
public TimePrinter(TalkingClock clock) {
outer = clock;
}
public void actionPerformed(ActionEvent e) {
if(outer.beep) { // outer会自动添加
System.out.println(e.getWhen());
}
}
}
}

内部类可以使private,也只有内部类可以是私有的,常规类只能是public。

内部类的特殊语法规则

上面说的outer,事实上更复杂些,表达式OuterClass.this表示外围类引用。例如:

// 外围类引用
// OuterClass.this
public void actionPerformed(ActionEvent e) {
if(TalkingClock.this.beep) { // TalkingClock.this.
System.out.println(e.getWhen());
}
}

// 内部类对象的构造器
// outerObect.new InnerClass(construction parameters)
ActionListener listener = this.new TimePrinter(); // 通常this.是多余的

// 外围类的作用域之外,可以这样引用内部类:
// OuterClass.InnerClass
TalkingClock clock = new TalkingClock(1000, true);
TalkingClock.TimePrinter printer = clock.new TimePrinter();

内部类中声明的所有静态字段都必须是final,并且初始化为一个编译时常量。如果不是常量就可能不唯一。 内部类不能有static方法。Java语言对此没有解释。

内部类是否有用、必要和安全

Java在1.1中增加内部类时,很多程序员都认为这是一项很重要的新特性,但这违背了Java要比C++更加简单的设计理念。 需要指出,内部类是一个编译器现象,与虚拟机无关。而虚拟机对此一无所知。 内部类转换成TalkingClock$TimePrinter.class

# 私有方法需要添加参数 javap -private TalkingClock\$TimePrinter.class 
$ javap TalkingClock\$TimePrinter.class
Compiled from "TalkingClock.java"
public class org.example.TalkingClock$TimePrinter implements java.awt.event.ActionListener {
final org.example.TalkingClock this$0;
public org.example.TalkingClock$TimePrinter(org.example.TalkingClock);
public void actionPerformed(java.awt.event.ActionEvent);
}

可以看到编译器生成了一个额外的this$0,对应外围类的引用,另外,构造器的TalkingClock参数。 如果编译器能够自动完成这个转换,能不能自己实现这个机制呢。 if(outer.beep) // 书上说这里有错,测试了一下,这里正常。 既然内部类会转换成名字古怪的常规类,内部类又如何得到哪些额外的访问权限呢?

javap TalkingClock.class 
Compiled from "TalkingClock.java"
public class org.example.TalkingClock {
public org.example.TalkingClock(int, boolean);
public void start();
static boolean access$000(org.example.TalkingClock);
}

access$000方法名可能不同,取决于编译器。 if(beep)转换为 if(TalkingClock.access$000(outer)) 这样做不是存在安全风险吗?这种担心是有道理的。 总而言之,如果内部类访问了私有数据字段,就有可能通过外围类所在的包中增加的其他类访问那些字段,但做这些需要技巧和决心。

合成构造器和方法可能非常复杂。假设将TimePrinter转换为一个私有内部类。在虚拟机中不存在私有类,因此生成一个私有构造器。 private TalkingClock$TimePrinter(TalkingClock); TalkingClock$TimePrinter(TalkingClock, TalkingClock$1); // 将调用上面的构造器 编译器将stat方法中转换为 new TalkingClock$TimePrinter(this, null)

局部内部类

public void start() {
public class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (beep) {
System.out.println(e.getWhen());
}
}
}

TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}

声明局部类时不能有访问说明符(即public或private)。局部类的作用域被限定在声明这个局部类的块中。除了start方法,没有任何方法知道TimePrinter类的存在。

由外部方法访问变量

public void start(boolean beep) {// TimePrinter类中除了可以访问外部类的字段,也可以访问方法的局部变量
public class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (beep) {
System.out.println(e.getWhen());
}
}
}

TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}

为了能够让actionPerformed方法中的代码工作,TimePrinter类在beep参数小时之前必须将beep字段复制为start方法的的局部变量。

$ javap TalkingClock\$1TimePrinter.class 
Compiled from "TalkingClock.java"
class org.example.TalkingClock$1TimePrinter implements java.awt.event.ActionListener {
final boolean val$beep;
final org.example.TalkingClock this$0;
org.example.TalkingClock$1TimePrinter();
public void actionPerformed(java.awt.event.ActionEvent);
}

beep参数会赋值给val$beep实例变量。

匿名内部类

public void start( boolean beep) {
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent e) {
if (beep) {
System.out.println(e.getWhen());
}
}
};
Timer timer = new Timer(interval, listener);
timer.start();
}

这个语法确实晦涩难懂,含义:创建一个类的新对象,这个类实现了ActionListener接口

new SuperType(construction parameters){ // 这里是构造器的参数
// inner class methods and data
}

SuperType可以是接口,也可以是类。

匿名内部类没有类名,所以没有构造器,实际上,构造器参数要给超类构造器。

new InterfaceType(){ // 接口需要有个括号,但是里面不能写内容
// methods and data
}

构造一个类的对象与构造一个扩展了那个类的匿名内部类的对象之间差别:

var queen = new Person("Mary");
var count = new Person("Dracula"){...};
// 匿名类不能有构造器,但可以提供一个对象初始化块
var count = new Person("Dracula"){
{ initialization }
...
};
// 如今做好的还是用lambda表达式
public void start( boolean beep) {
Timer timer = new Timer(interval, event -> {
if (beep) {
System.out.println(event.getWhen());
}
});
timer.start();
}
// 下面的技巧称为“双括号初始化” 利用了内部类的语法。
var friends = new ArrayList<String>();
friends.add("Harry");
friends.add("Tony");
invite(friends);
// 这里是ArrayList的内部类
invite(new ArrayList<String>(){{add("Harry");add("Tony");}});
// 实际中这个技巧很少使用。一般直接传入
List.of("Harry", "Tony");
// 匿名子类的equals方法要特别当心。
if(getClass() != other.getClass()) return false;
// 这个测试对匿名子类会失败
// 生成日志或调试消息时,通常希望包含类名
System.out.println(getClass());
// 这对静态方法不生效,调用getClass时调用的是this.getClass(),而静态方法没有this。
new Object(){}.getClass().getEnclosingClass();
// new Object(){}会建立Object的匿名子类中的一个匿名对象,getEnclosingClass得到其外围类,也就是包含这个静态方法的类。

静态内部类

有时候使用内部类只是为了吧一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。就可以声明为static

例子:计算数组中的自小时和最大值

当然可以编写两个方法,但是调用两个方法的时候遍历了两次数组,如果能遍历一次则可提高效率

double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for(double v : values){
if(min > v) min = v;
if(max < v) max = v;
}

// 把最大值和最小值封装到一个类里面
class Pair {
private double first;
private double second;
public Pair(double f, double s){
first = f;
second = s;
}
public double getFirst(){return first;}
public double getSecond(){return second;}
}

class ArrayAlg {
public static Pair minmax(double[] values) {
...
return new Pair(min, max);
}
}

// 调用者
Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());

// 可以调整为内部类
ArrayAlg.Pair p = ArrayAlg.minmax(d);

class ArrayAlg {
public static class Pair { // 只有内部类可以是static
...
}
...
}

静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成他的外围类对象的引用。在示例中使用静态内部类,这是由于内部类对象实在静态方法中构造的。不是静态的将会报错,指出没有可用的隐式ArrayAlg类型对象来初始化内部类对象。

只要内部类不需要访问外围类对象,就应该使用静态内部类。

与常规内部类不同,静态内部类可以有静态方法和字段。

在接口中声明的内部类自动是static和public

6.4 服务加载器

​ 有时会开发一个服务架构的应用。有些平台支持这种方法,如OSGi,可以用于开发环境、应用服务器和其他复杂的应用。不过JDK还提供了一个加载服务的简单机制。这种机制由Java平台模块系统提供支持。

​ 程序员希望提供的服务能有一些自由,能够确定如何实现服务的特性。另外还希望有多个实现供选择。使用ServiceLoader类可以很容易地加载符合一个公共接口的服务。

定义一个接口包含服务的各个实例应当提供的方法。例如,假设你的服务要提供加密。

package serviceLoader;
public interface Cipher {
byte[] encrypt(byte[] source, byte[]key);
byte[] decrypt(byte[] source, byte[]key);
int strength();
}

实现类必须有一个无参构造器。

package serviceLoader.impl;
public class CaesarCipher implements Cipher {
byte[] encrypt(byte[] source, byte[]key) {
...
}
byte[] decrypt(byte[] source, byte[]key) {
...
}
int strength() {
...
}
}

现在把这些类的类名增加到META-INF/services目录下的一个UTF-8编码文本文件中,文件名必须跟限定名一致。

serviceLoader.impl.CaesarCipher

这个例子中提供了一个实现类。还可以提供多个供选择。

准备工作之后,可以初始化一个服务加载器:

public static ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher);

这个工作只做一次。

服务加载器的iterator方法会返回一个迭代器来迭代处理所提供的所有服务实现。

public static Cipher getCipher(int minStrength) {
for(Cipher cipher : cipherLoader) {
if(cipher.strength() >= minStrength) return cipher;
}
return null;
}

也可以使用流查找所要的服务。stream方法会生成ServiceLoader.Provider实例的一个流。这个接口包含type和get方法,可用来得到提供者和提供者实例。如果按类型选择一个提供者,只需要调用type,没有必要实例化任何服务实例。

public static Optional<Cipher> getCipher2(int minStrength) {
return cipherLoader.stream()
.filter(descr -> descr.type() == ServiceLoader.impl.CaesarCipher.class)
.findFirst()
.map(ServiceLoader.Provider::get);
}
// 如果想要得到任何服务实例,只需要调用findFirst
Optional<Cipher> cipher = cipherLoader.findFirst();

6.5 代理

利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。

(指的是动态代理,相关文章Java代理详解

如何使用代理

假设你想构造一个类的对象,这个类实现了一个或多个接口,但是在编译时你可能并不知道这些接口是什么。这个问题有些难度。要想构造一个具体的类,只需要使用newInstance方法或者反射构造器。但是不能实例化接口。需要在运行的程序中定义一个新类。

为了解决这个问题,有些程序会生成代码,将这些代码放在一个文件中,调用编译器,然后在加载得到类文件。这样会很慢,代理机制则是一个更好的解决方案。代理类在运行时创建全新的类。这一样的代理类能够实现你指定的接口。

调用处理器是实现了InvocationHandler接口的类的对象。无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。

创建代理对象

创建代理对象需要使用Proxy的newProxyInstance方法。这个方法有三个参数。

  • 一个类加载器。
  • 一个Class对象数组,每个元素对应需要实现的各个接口。
  • 一个调用处理器。
public class TraceHandler implements InvocationHandler {
private Object target;

public TraceHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print(target);
System.out.print("." + method.getName() + "(");
if (args != null) {

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);
}
}

public class ProxyTest {
public static void main(String[] args) {
Object[] elements = new Object[100];
for (int i = 0; i<elements.length; i++) {
Integer value = i+1;
TraceHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class}, handler
);
elements[i] = proxy;
}

int key = new Random().nextInt(elements.length) + 1;
int result = Arrays.binarySearch(elements, key);
if (result >= 0) {
System.out.println(elements[result]);
}
}
}

Integer类实现了Comparable接口。代理对象属于运行时定义的一个类。这个类也实现了Comparable解耦,不过,他的compareTo方法调用了代理对象处理器的invoke方法。

注意尽管toString方法不属于Comparable接口也会 被代理一下。

代理类的特征

一旦代理类被创建就变成了常规类,与其他普通类没有区别。

所有代理类都要覆盖Object类的toString、equals和hashCode方法。然而clone和getClass没有重新定义。

对于一个铁定的类加载器和预设的一组接口来说,只能有一个代理类。如果调用两次newProxyInstance方法,得到的是同一个。

代理类总是public和final。

至此,面向对象特性就介绍完毕了,接口、lambda表达式和内部类是经常会遇到的几个概念,克隆、服务加载器和代理等高级技术主要是设计库及构建工具的程序员感兴趣对应用开发程序员吸引力不大。

7 异常、断言和日志

  • 向用户通知错误;
  • 保存所有工作;
  • 允许用户妥善地退出程序。

Java使用了一种称为异常处理(exception handing)的错误捕获机制。

​ 在测试期间,需要运行大量的检查以确保程序的正确性。然而,这些检查可能非常耗时,在测试完成后也没必要保留。也可以简单地将这些检查删除,需要另做测试的时候再粘贴回来,不过这样操作会很烦琐。使用断言有选择地启动检查。

​ 当程序出现错误时,并不是总能够与用户或终端进行通信。此时,我们可能希望记录出现的问题,以备日后进行分析。

7.1 处理错误

  • 返回到一种安全状态,并能够让用户执行安全命令;或者
  • 允许用户保存所有工作的结果,并以妥善的方式终止程序。

需要考虑的问题:

  • 用户输入错误:例如格式错误
  • 设备错误:设备故障
  • 物理限制。磁盘已满
  • 代码错误。

​ 对于方法中的错误,传统的做法是返回一个特殊的错误码,由调用方法分析。遗憾的是,并不是任何情况下都能够返回一个错误码。有可能无法明确地将有效数据与无效数据加以区分。例如-1标识错误但也有可能是一个合法的结果。

​ 在这种情况下,方法并不返回任何值,而是抛出一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回正常值(或任何值)。此外也不会调用这个方法的代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常情况的异常处理器。异常有自己的语法和特定的层次结构。

异常分类

37C0A95023E2E3BB2CA122579EA81F9C.png

所有异常都是由Throwable继承而来。Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。你的应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力妥善地终止程序之外,你几乎无能为力。这种情况很少出现。

​ 程序设计时重点关注Exception层次结构。有两个分支:RuntimeException(编程错误导致)、IOException(程序没问题,像IO错误这类问题导致的异常)

派生于RuntimeException的异常包括以下问题:

  • 错误的强制类型转换
  • 数组访问越界
  • 访问null指针

不是派生于RuntimeException的异常:

  • 试图超越文件末尾继续读取数据。
  • 试图打开一个不存在的文件
  • 试图根据给定的的字符串查找Class对象,而这个字符串表示的类并不存在。

​ “如果出现RuntimeException异常,那么就一定是你的问题”。应该通过检测数组下标是否越界避免ArrayIndexOutOfBoundsException异常,应该在使用变量前通过检测是否为null来杜绝NullPointerException异常的发生。

​ 如何处理不存在的文件呢?难道不能先检查文件是否存在再打开它吗?这个文件有可能在你检查之后就被立即删除了。因此“是否存在”取决于环境,而不只是取决于你的代码。

​ Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常(unchecked),所有其他的异常称为检查型异常(checked)。编译器将检查你是否为所有检查型异常提供了异常处理类。

声明检查型异常

public FileInputStream(String name) throws FileNotFoundException
public Image loadImage(String s) throws FileNotFoundException, EOFException

不需要声明Java的内部错误,即从Error集成的异常,例如ArrayIndexOutOfBoundsException。

总之,一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在你的控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)。如果你的方法没有声明所有可能发生的检查型异常,编译器就会发出一个错误消息。

如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类中声明的异常更通用。

如何抛出异常

public String readData(String name) throws Exception {
if ("".equals(name)) {
throw new Exception(gripe);
}
return "";
}

创建异常类

public class FileFormatException extends IOException {
public FileFormatException() {
super();
}

public FileFormatException(String message) {
super(message);
}
}

7.2 捕获异常

try {
code
} catch(ExceptionType e) {
handler for this type
}

一般经验是,要捕获那些你知道如何处理的异常,而继续传播那些你不知道怎样处理的异常。

捕获多个异常

try {
code that might throw exceptions
} catch(FileNotFoundException | UnknownHostException e) {
// 缺少文件和未知主机异常的动作是一样的
} catch(IOException e) {

}

捕获多个异常不仅会让你的代码看起来更简单,还会更高效。生成的字节码只包含对应公共catch子句的一个代码块。

再次抛出异常与异常链

try {
access the database
} catch (SQLException e) {
throw new ServletException("database error: " + e.getMessage());
}

更好的处理方法,可以把原始异常设置为新异常的“原因”

try {
access the database
} catch (SQLException original) {
var e = new ServletException("database error");
e.initCause(original);
throw e;
}

捕获异常时,可以使用下面语句获取原始异常:

Throwable original = caughException.getCause();

强烈建议使用这种包装技术。这样可以在子系统中抛出高层异常,而不会丢失原始异常的细节。

有时只想记录一下而不作其他改变。

try {
access the database
} catch (Exception e) {
logger.log(level, message, e);
throw e;
}

finally子句

try {
access the database
} catch (Exception e) {
logger.log(level, message, e);
throw e;
} finally {
in.close();
}
try {
try {
code that might throw exceptions
} finally {
in.close();
}
} catch(IOException e) {
show error message
}

内层的try语句只有一个职责,就是确保关闭输入流。外层的try语句块只有一个职责,就是确保报告出现的错误。

自己感觉上面这种嵌套就是一种代码风格

finally语句中不要使用return

try-with-Resources语句

try(Resource res = ...) {
work with res
}

try块退出时,会自动调用res.close()。

try(var in = new Scanner(
new FileInputStream("/usr/share/dict/words"), StandardCharsets.UTF_8)) {
while(in.hasNext()){
System.out.println(in.next());
}
}

指定多个资源

try(var in = new Scanner(
new FileInputStream("/usr/share/dict/words"), StandardCharsets.UTF_8);
var out = new PrintWriter("out.txt", StandardCharsets.UTF_8)) {
while(in.hasNext()){
out.println(in.next().toUpperCase());
}
}

不论这个块如何退出,in和out都会关闭。

分析堆栈轨迹元素

堆栈轨迹(stack trace)

printStackTrace方法。

StackWalker walker = StackWalker.getInstance();

walker.forEach(frame -> analyze frame)

7.3 使用异常的技巧

7.4 使用断言

7.5 日志

7.6 调试技巧

8 泛型程序设计

9 集合

集合框架

10 图形用户界面程序设计

11 Swing用户界面组件

12 并发