1 Java 程序设计概述 Java 的 11 个关键字:
简单性
面向对象
分布式
健壮性
安全性
体系结构中立
可移植性
解释性
高性能
多线程
动态性
2 Java 环境安装 术语:
Java Develepment Kit(JDK):编写 Java 程序的程序员使用的软件
Java Runtime Environment(JRE):运行 Java 程序的用户使用的软件
Standard Edition(SE):用于桌面或简单服务器应用的 Java 平台
Enterprise Edition(EE):用于复杂服务器应用的 Java 平台
Micro Edition(ME):用于手机和其他小型设备的 Java 平台
OpenJDK:Java SE 的开源实现
Java Downloads | Oracle
2.1 Windows
安装包下载并安装(需要账号),jdk-8u301-windows-x64.exe
配置 JAVA_HOME 系统变量,变量值为 C:\env\jdk1.8
(JDK安装目录)
配置 PATH 系统变量,添加 %JAVA_HOME%\bin
、%JAVA_HOME%\jre\bin
测试是否配置完成,java -version
、javac -version
2.2 Linux 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 sudo tar zxf jdk-8u301-linux-x64.tar.gz -C /opt vim /etc/profileexport JAVA_HOME=/opt/jdk1.8.0_301export PATH=$JAVA_HOME /bin:$PATH export PATH=$JAVA_HOME /jre/bin:$PATH source /etc/profile java -version javac -version
2.3 HelloWorld 1、创建文件 HelloWorld.java 文件(严格区分大小写)
1 2 3 4 5 public class HelloWorld { public static void main (String[] args) { System.out.println("Hello World, Java 8!" ); } }
2、使用 javac 编译 Java 文件,javac HelloWorld.java
3、执行(没有任何后缀,直接就是 HelloWorld),java HelloWorld
3 Java 基础程序设计 3.1 注释
单行注释:// 开始到本行结尾
多行注释:/* 开始,以 */ 结束
文档注释:/** 开始,以 */ 结束
1 2 3 4 5 6 7 8 9 10 11 12 13 public class CommentsTest { public static void main (String[] args) { System.out.println("We will not use 'Hello, World!'" ); } }
3.2 数据类型 3.2.1 整型
类型
存储需求
取值范围
int
4 字节
-2^31 ~ 2^31-1
short
2 字节
-2^7 ~ 2^7-1
long
8 字节
-2^63 ~ 2^63-1
byte
1 字节
-128 ~ 127
1 2 3 4 5 6 7 8 9 10 11 12 13 14 long a = 4000000000L ;int b = 0xa1f ;int c = 0173 ;int d = 0b1001 ;int e = 0b1111_0100_0010_0100_0000 ;
3.2.2 浮点型
类型
存储需求
取值范围
float
4 字节
double
8 字节
1 2 3 4 5 6 7 8 9 float a = 1010.101010f ;double b = 10.101010231 ;double c = 0x1.0p-3 ;
正无穷大,Double.POSITIVE_INFINITY
负无穷大,Double.NEGATIVE_INFINITY
NaN,Double.NaN
浮点数不适合用于无法接受舍入误差的金融计算,应该使用 BigDecimal 类
3.2.3 char 类型 char 类型的字面量值需要用单引号括起来。char 类型的值也可以表示为十六进制值。
转义序列
名称
Unicode 值
\b
退格
\u0008
\t
制表
\u0009
\n
换行
\u00a
\r
回车
\u00d
\“
双引号
\u0022
\‘
单引号
\u0027
\\
反斜杠
\u005c
Unicode 转义序列会在解析代码之前处理,例如 // \u00A0 is a newline
,由于 \u00A0
会替换成一个换行符,因此会产生语法错误
char 类型描述了 UTF-16 编码中的一个代码单元,占 2 个字节
不建议使用 char 类型,除非需要处理 UTF-16 代码单元
3.2.4 boolean 类型 boolean 类型有两个值:false 和 true。整型值和布尔值不能相互转换
3.3 变量 声明变量为,变量类型+变量名,例如 int a;
。变量名不能以数字开头的,由数字、字母、_、$组成。大小写敏感,长度没有限制。
尽管 $ 是合法的命名字符,但不要个人使用,它只用在 Java 编译器或其它工具生成的名字中
不能使用 Java 保留字作为变量名
3.3.1 变量初始化 声明变量后,必须使用赋值语句进行显式初始化,例如 int a = 10;
,千万不要使用未初始化的变量。变量的声明尽可能靠近变量第一次使用的地方。
Java 10 开始如果变量能推断出类型可以使用 var 来声明变量。
3.3.2 常量 使用 final 指定常量,常量只能被赋值一次,无法修改,static final 指定类常量。
1 2 3 4 5 6 7 8 9 10 11 public class Constants { public static final double COUNT = 10 ; public static void main (String[] args) { final double PI = 3.14 ; System.out.println(PI + Constants.COUNT); System.out.println(PI + COUNT); } }
3.4 运算符 算术运算符 +、-、*、/、% 表示加、减、乘、除、求余(取模)运算
1 2 3 4 5 6 7 int a = 10 / 3 ;int b = 10 / 3.0 ;
3.4.1 数学函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MathTest { public static void main (String[] args) { double a = Math.sqrt(4 ); double b = Math.pow(2 , 2 ); int c = Math.floorMod(13 , 12 ); System.out.println(Math.PI); System.out.println(a); System.out.println(b); System.out.println(c); } }
3.4.2 数值类型转换 int,long 转为 float 以及 long 转 double 会有精度损失。
二元运算中,两个操作数有一个 double,另一个也转 double;否则,有一个 float,另一个转 float;否则,有一个 long,另一个转 long;否则两个都转为 int。
3.4.3 强制类型转换 强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。
1 2 3 double a = 9.997 ;int b = (int )a;
3.4.4 赋值运算符
3.4.5 自增自减运算符 1 2 3 4 5 6 7 8 int m = 7 ;int n = 7 ;int a = 2 * ++m; int b = 2 * n++;
3.4.6 关系运算符 ==、!=、<、<=、>=、&&(短路与)、||(短路或)
expression1 && expression2
,如果第一个表达式会 false,就不会再管第二个表达式
expression1 || expression2
,如果第一个表达式会 true,就不会再管第二个表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class OperatorTest { public static void main (String[] args) { int a = 10 ; int b = 11 ; if (a > 10 && (b+=1 ) > 12 ){ } System.out.println(a); System.out.println(b); if ((a+=1 ) > 11 || (b+=1 ) > 11 ){ } System.out.println(a); System.out.println(b); } }
3.4.7 位运算 &(and)、|(or)、^(xor)、~(not),这些运算符按位模式处理。& 和 | 也能返回布尔值,不采用短路方式求值。
>>(逻辑右移,除 2),<<(逻辑左移,乘 2),>>>(算术右移,符号位填充高位),不存在 <<< 运算符。
3.4.8 运算符级别
运算符
结合性
[]、()
左 -> 右
!、~、++、–、+(一元运算)、-(一元运算)、()(强制类型转换)、new
右 -> 左
*、/、%
左 -> 右
+、-
左 -> 右
<<、>>、>>>
左 -> 右
<、<=、>、>=、instanceof
左 -> 右
==、!=
左 -> 右
&
左 -> 右
^
左 -> 右
|
左 -> 右
&&
左 -> 右
||
左 -> 右
?:
右 -> 左
=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、>>>=
右 -> 左
3.4.9 枚举类型 1 2 3 enum Size { SMALL, MEDIUM, LARGE};Size s = Size.SMALL;
3.5 字符串 字符串从概念上来说即是 Unicode 字符序列,使用双引号括起来,字符串都是 String 类的一个实例。字符串不可变,无法修改。
不可修改的优点就是编译器可以让字符串共享,存在一个字符串池,如果字符串相同则直接引用。
实际上只有字符串字面量是共享的,而 + 或 substring 等操作产生的字符串不共享。
3.5.1 子串 1 2 3 4 5 String s = "Hello" ;String sub = s.substring(1 , 3 );
3.5.2 拼接 1 2 3 4 5 6 String s1 = "Hello" + ",World!" ;String s2 = "Hello" + "123" ;String all = String.join("/" , "S" , "L" ,"M" );
3.5.3 检测相等
千万不要使用 == 运算符比较字符串是否相等,这个运算符是比较两个字符串的地址是否相等
3.5.4 空串与 Null 1 2 3 4 5 if (str != null && str.length() != 0 )
3.5.5 码点与代码单元 1 2 3 4 5 6 7 8 int [] codePoints = str.codePoints.toArray();
3.5.6 String API 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
3.5.7 构建字符串 每次连接字符串都是构建一个新的 String 对象,可以使用 StringBuilder 高效创建字符串。StringBuffer 有线程同步机制,但效率低,StringBuilder 线程不安全,效率高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class StringBuilderTest { public static void main (String[] args) { StringBuilder builder = new StringBuilder (); builder.append("welcome" ); builder.append(" new" ); builder.append(" world" ); builder.append(", CoreJava" ); builder.delete(8 , 11 ); builder.insert(7 , " to the" ); System.out.println(builder.length()); String completedString = builder.toString(); System.out.println(completedString); } }
3.6 输入输出 3.6.1 读取输入 使用 Scanner 类完成键盘读取输入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.*;public class InputTest { public static void main (String[] args) { Scanner in = new Scanner (System.in); System.out.print("What's your name?" ); String name = in.nextLine(); System.out.print("How old are you?" ); int age = in.nextInt(); System.out.print("How much are your salary every month?" ); double salary = in.nextDouble(); System.out.println("Hello," + name + ".Next year, you'll be " + (age+1 ) + ".And if you don't take any money, you'll save " + (salary * 12 )); } }
3.6.2 格式化输出 使用 printf 进行格式化输出,也可以使用 String.format()。转换符表如下:
转换符
类型
举例
d
十进制整数
159
x
十六进制整数
9f
o
八进制整数
237
e
定点浮点数
15.9
g
通用浮点数
–
a
十六进制浮点数
0x1.fccdp3
s
字符串
Hello
c
字符
H
b
布尔
True
h
散列码
42628b2
tx、Tx
日期时间(T 强制大写)
已经过时的
%
百分号
%
n
与平台有关的行分隔符
–
3.7 控制流程 Java 没有 goto 语句,但是 break 语句可以带标签,达到从内存循环跳出的目的。
3.7.1 块作用域 一个块中可以嵌套另一个块,但是不能在嵌套的两个块中声明同名的变量。
1 2 3 4 5 6 7 8 9 10 public class BlockTest { public static void main (String[] args) { int n = 1 ; { int k = 1 ; int n = 2 ; } System.out.println(n); } }
3.7.2 条件语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 if (condition) statement;if (condition){ statement1; statement2; ... }if (condition) statement1 else statement2;if (condition){ statemen1; statemen2; }else { statement3; statement4; }if (condition1){ statement1; }else if (condition2){ statement2; }else { statement3; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.*;public class IfTest { public static void main (String[] args) { Scanner in = new Scanner (System.in); System.out.println("How much are your salary?" ); double salary = in.nextDouble(); double target = 4000 ; String performance = "" ; if (salary >= 2 * target){ performance = "Excellent" ; }else if (salary >= 1.5 * target){ performance = "Fine" ; }else if (salary >= target){ performance = "Satisfactory" ; } else { System.out.println("You're fired" ); } System.out.println(performance + "~" ); } }
3.7.3 循环语句 1 2 3 4 5 while (condition) statement;do statement while (condition) ;
3.7.4 for 循环
for 语句第一部分是计数器初始化,第二部分是循环条件,第三部分是如何更新计数器
for 语句内部定义的变量外部无法使用,每个独立的 for 语句可以定义同名变量
1 2 3 4 5 6 7 8 9 10 11 12 public class ForTest { public static void main (String[] args) { for (int i = 1 ; i <= 10 ; i++){ System.out.print(i + " " ); } System.out.println(); for (int i = 0 ; i < 10 ; i++){ System.out.print(i + " " ); } } }
3.7.5 switch 语句 switch 语句从选择项匹配的 case 标签处开始执行直到遇到 break 语句或执行到 switch 语句结尾处。如果没有匹配的而有 default 语句,就执行 default 子句。如果某个 case 分支没有 break 语句,就有可能继续执行下一个 case 分支。
case 标签可以是:
类型为 char、byte、short、int 的常量表达式
枚举常量
字符串字面量(Java SE 7 开始)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class SwitchTest { public static void main (String[] args) { Scanner in = new Scanner (System.in); System.out.println("请输入你想使用的功能:(1,2)" ); String choice = in.next(); switch (choice){ case "1" : System.out.println("你选择了 1 功能,什么也没发生" ); break ; case "2" : System.out.println("你选择了 2 功能,什么也没发生" ); break ; default : System.out.println("没有该功能..." ); } } }
3.7.6 中断控制语句
break:跳出当前循环
break tag:跳出循环,从内到外,跳出语句块,到 tag 位置
continue:跳出当前循环,继续下一次循环
3.8 大数值 java.math 包中有 BigInteger 和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 实现任意精度的整数运算,BigDecimal 实现任意精度的浮点运算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.math.*;public class BigTest { public static void main (String[] args) { BigInteger a = BigInteger.valueOf(100 ); BigInteger b = BigInteger.valueOf(200 ); System.out.println(a.add(b)); System.out.println(a.subtract(b)); System.out.println(a.multiply(b)); System.out.println(a.divide(b)); System.out.println(a.mod(b)); System.out.println(a.compareTo(b)); } }
3.9 数组 数组是一种数据结构,用来存储同一类型值的集合。通过整型下标可以访问数组的每一个值。声明数组时,需要指出数组类型和数组变量的名字,int[] a 或 int a[]
,初始化使用 new 运算符,int[] a = new int[10]
,其中 10 表示数组长度,不要求是常量,数组一但初始化长度就不能再修改大小。
创建数字数组时,所有元素都初始化 0
创建 boolean 数组时,元素都初始化为 false
创建对象数组,元素都初始化为 null,表示还未存放对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class IntArrayTest { public static void main (String[] args) { int [] arr = new int [10 ]; for (int i = 0 ; i < arr.length; i++){ arr[i] = i * i; } for (int i = 0 ; i < arr.length; i++){ System.out.print(arr[i] + " " ); } } }
3.9.1 数组遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.*;public class IntArrayTest { public static void main (String[] args) { int [] arr = {1 , 2 , 3 , 4 , 5 , 6 , 7 }; for (int i = 0 ; i < arr.length; i++){ System.out.print(arr[i] + " " ); } for (int a: arr){ System.out.print(a + " " ); } System.out.println(Arrays.toString(arr)); } }
3.9.2 数组拷贝
int[] a = new int[10]; int[] b = a;
b 和 a 引用的是一个同一个数组
如果需要所有值拷贝到新数组则需要使用 Arrays.copyOf() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.*;public class IntArrayTest { public static void main (String[] args) { int [] arr = {1 , 2 , 3 , 4 , 5 , 6 , 7 }; int [] arr1 = arr; System.out.println(arr); System.out.println(arr1); arr1[0 ] = 100 ; System.out.println(Arrays.toString(arr)); System.out.println(Arrays.toString(arr1)); int [] arr2 = Arrays.copyOf(arr, 2 * arr.length); int [] arr3 = Arrays.copyOf(arr, 3 ); System.out.println(Arrays.toString(arr2)); System.out.println(Arrays.toString(arr3)); System.out.println(arr2); System.out.println(arr); arr2[0 ] = 100 ; System.out.println(Arrays.toString(arr)); System.out.println(Arrays.toString(arr2)); } }
3.9.3 命令行参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ArgsTest { public static void main (String[] args) { for (int i = 0 ; i < args.length; i++){ System.out.println("args[" + i + "]: " + args[i]); } } }
3.9.4 数组排序 可以使用 Arrays.sort(),对数组进行排序
3.9.6 多维数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 double [][] a;double [][] a = nwe double [10 ][10 ];double [][] a = { {1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 } }import java.util.*;public class MultiArrayTest { public static void main (String[] args) { int [][] a = { {1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 } }; for (int i = 0 ; i < a.length; i++){ for (int j = 0 ; j < a[i].length; j++){ System.out.print(a[i][j] + " " ); } System.out.println(); } for (int [] nums: a){ for (int num: nums){ System.out.print(num + " " ); } System.out.println(); } System.out.println(Arrays.deepToString(a)); } }
3.9.7 不规则数组 Java 实际上没有多维数组,只有一维数组。多维数组其实是“数组的数组”,a[i] 处存放的数组的引用,因此可以方便的构造“不规则”数组,即数组的每一行有不同的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.util.*;public class IrregularArrayTest { public static void main (String[] args) { int [][] arr = new int [5 ][]; for (int i = 0 ; i < arr.length; i++){ arr[i] = new int [i + 1 ]; } for (int [] nums: arr){ for (int num: nums){ System.out.print(num + " " ); } System.out.println(); } } }
4 对象与类 4.1 OOP 概述 4.1.1 类 类是构造对象的模板或蓝图,由类构造对象的过程成为创建类的实例。
封装是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域,操纵数据的过程称为方法。封装的关键在于绝不能让类中的方法直接访问其他类的实例域。
继承是扩展一个已有的类,并且新类具有所扩展类的全部属性和方法,并且新类可以提供新类的新方法和数据域。Java 中所有类都继承 Object。
4.1.2 对象
对象的行为 —— 可以对对象应用哪些方法?
对象的状态 —— 当调用方法时,对象如何响应?
对象标识 —— 如何区分具有相同行为与状态的不同对象
每个对象都保存着描述当前特征的信息。对象的状态必须通过调用方法实现(如果不是通过方法调用能改变对象的状态,封装则被破坏了)。作为类的实例,每个类的标识永远不同。
4.1.3 识别类 识别类的简单规则时分析问题的过程中寻找名词,而方法对应着动词。在创建类的时候,哪些是名词和动词是重要的完全取决于个人的开发经验。
4.1.4 类之间的关系
依赖(uses-a):一个类的方法操纵另一个类的对象。应该尽可能将相互依赖的类减至最小
聚合(has-a):类 A 的对象包含类 B 的对象,比如 Order 对象包含 Item 类
继承(is-a):类 A 扩展 类 B,则类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能
通常使用 UML(Unified Modeling Language)统一建模语言绘制类图,描述类之间的关系。
4.2 预定义类 4.2.1 对象与对象变量 使用对象之前,必须构造对象并指定其初始状态。通过构造器构造实例,构造器是一个特殊方法,用来构造并初始化对象。
构造器的名字应该与类名相同,构造对象需要在构造器前加上 new 操作符。构造的对象可以赋给变量对此使用,声明一个类变量时如果没有初始化对象则无法调用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 new Date ();new Date ().toString();Date date = new Date (); Date date; date.toString(); date = null ;
4.2.2 LocalDate 类 LocalDate 类采用熟悉的日历表示法。
更改器方法,调用该方法对象状态会发生改变
访问器方法,只访问对象而不修改对象的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import java.time.*;public class CalendarTest { public static void main (String[] args) { LocalDate date = LocalDate.now(); int month = date.getMonthValue(); int day = date.getDayOfMonth(); date = date.minusDays(day - 1 ); DayOfWeek weekday = date.getDayOfWeek(); int value = weekday.getValue(); System.out.println("Mon Tue Wed Thu Fri Sat Sun" ); for (int i = 1 ; i < value; i++){ System.out.print(" " ); } while (date.getMonthValue() == month){ System.out.printf("%3d" , date.getDayOfMonth()); if (date.getDayOfMonth() == day){ System.out.print("*" ); }else { System.out.print(" " ); } date = date.plusDays(1 ); if (date.getDayOfWeek().getValue() == 1 ){ System.out.println(); } } if (date.getDayOfWeek().getValue() != 1 ){ System.out.println(); } } }
4.3 自定义类 4.3.1 Employee 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.time.*;public class Employee { private String name; private double salary; private LocalDate hireDay; public Employee () { } 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; } }
类的所有方法都标记为 public,意味着任何类的任何方法都能调用这个方法。
三个实例域都用 private 修饰,意味着只有 Employee 自身能访问这些实例域,其他类方法不能。强烈建议实例域标记为 private。
4.3.2 构造器 构造器总是伴随着 new 操作符被调用,而不能对一个已经存在的对象调用构造器到达重新设置实例域的目的。
构造器与类同名
每个类可以有一个以上的构造器
构造器可以有 0 个、1 个或多个参数
构造器没有返回值
构造器总是伴随着 new 操作符一起调用
类默认会有一个无参数构造器方法,当定义了有参数的构造器方法,默认的无参构造器就无了,需要显式定义
4.3.3 隐式参数和显式参数 在每一个方法中,关键字 this 代表隐式参数,代表当前对象
1 2 3 4 public void raiseSalary (double byPercent) { double raise = this .salary * byPercent / 100 ; this .salary += raise; }
4.3.4 封装的优点 实例域进行私有化,提供域访问器和域更改器方法有两个优点,一除了类的方法之外,不会影响其他代码。二更改器方法能执行错误检查。不要返回引用可变的对象的访问器方法,如果需要可变对象引用应该克隆。
不要编写返回可变对象引用的访问器方法。若需要返回一个可变对象的引用,应将其克隆返回。
4.3.5 私有方法 将方法的修饰为 private,外部则无法调用,类的辅助方法通常不需要在外部调用声明为私有方法
4.3.6 final 实例域 将实例域定义为 final,构建对象时必须初始化这样的域,且后面的操作无法修改该域。
4.4 静态域和静态方法 4.4.1 静态域 如果实例域定义为 static,那么每个类只有一个这样的域,每个对象的所有实例域都有自己的一份拷贝。它属于类,不属于任何对象。
4.4.2 静态常量 static final 修饰,例如 Math.PI,System.out。
4.4.3 静态方法 静态方法是一种不能向对象实施操作的方法。建议使用类名调用静态方法不造成混淆。以下两种情况使用静态方法。
一个方法不需要访问对象状态,其所需参数都是以通过显式参数提供
一个方法只需要访问类的静态域
4.4.4 工厂方法 静态工厂方法,用于构造不同的对象。
4.4.5 main 方法 main 方法不对任何对象进行操作,静态 main 方法将执行并创建程序所需要的对象。main 可用来做单元测试。
4.5 方法参数 Java 中,总是采用按值调用。方法得到的是所有参数的拷贝。
一个方法不能修改一个基本数据类型的参数
一个方法可以改变一个对象参数的状态
一个方法不能让对象参数引用一个新的对象
4.6 对象构造 4.6.1 重载 多个方法,相同的名字、不同的参数,便产生了重载。要完整的描述一个方法,需要指定方法名和参数类型,返回类型不是其中的一部分,因此不能有两个名字相同、参数类型相同但返回类型不同的方法。
4.6.2 默认域初始化 如果构造器中没有给实例域初始化,则自动初始化:数值为 0、布尔为 false、对象引用为 null。
4.6.3 显式域初始化 在实例域定义时就给其赋一个值
1 2 3 class Employee { private String name = "" ; }
4.6.4 this 使用 1 2 3 4 5 6 7 8 9 10 11 public Employee (String name, double salary) { this .name = name; this .salary = salary; }public Employee (double salary) { this ("Employee #" + nextId, salary); nextId++; }
4.6.5 初始化块 首先执行初始化块,然后再运行构造器。
静态初始化块,在类第一次加载的时候,会进行静态域的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Employee { private static int nextId; private int id; private String name = "" ; private double salary; static { System.out.println("静态初始化代码块" ); nextId = new Random ().nextInt(10000 ); } { System.out.println("初始化代码块" ); id = nextId; nextId++; } public Employee (String name, double salary) { System.out.println(name + "两个参数的构造方法" ); this .name = name; this .salary = salary; } public Employee (double salary) { this ("Employee #" + nextId, salary); } public Employee () {} public String getName () { return name; } public double getSalary () { return salary; } public int getId () { return id; } }
4.7 包 4.7.1 类的导入 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.*;import java.util.*;import java.sql.*;import java.util.Date;new java .util.Date();
4.7.2 静态导入 导入静态方法和静态字段
1 2 3 4 5 6 7 8 import static java.lang.Math.*;public class StaticImportTest { public static void main (String[] args) { int a = 10 ; System.out.println(pow(a, 2 )); } }
4.7.3 组织类 类文件开头,写上 package 包名;
1 2 javac top/reajason/PayrollApp.java java top.reajason.PayrollApp
4.8 类路径 unix:/home/user/classdir:.:/home/user/archives/archive.jar
windows:c:\classdir;.;c:\archives\archive.jar
类路径所列出的目录和归档文件是搜寻类的起始点,默认类路径包含 . (当前目录)。
Java 6 开始可在 JAR 文件目录指定通配符,例如(archives 中所有 JAR 文件都包含到类路径中):
windows:c:\classdir;.;c:\archives\*
java.lang 包被默认导入。
4.8.1 设置类路径 java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyApp
不要将 CLASSPATH 设置成全局变量。
4.9 文档注释 javadoc 能将源文件生成一个 HTML 文档。
javadoc 将在以下中抽取信息:
包
公有类与接口
公有的和受保护的构造器及方法
共有的和受保护的域
4.9.1 类注释 类注释必须放在 import 语句之后,类定义之前。
1 2 3 4 5 6 public class Card { }
4.9.2 方法注释 每一个方法注释必须放在所描述的方法之前。
@param 变量描述,可占据多行,一个方法的所有 @param 标记必须放在一起
@return 描述,可占据多行
@throws 类描述,表示方法可能抛出的异常
1 2 3 4 5 6 7 8 9 10 public double raiseSalary (double byPercent) { double raise = salary * byPercent / 100 ; salary += raise; return raise; }
4.9.3 域注释 只建立文档需要对公共域(通常指静态常量)
1 2 3 4 public static final int RATES = 5 ;
4.9.4 通用注释
@author 姓名:可以使用多个
@version 文本:版本描述
@since 文本:引入特性的描述
@deprecated 文本:不再使用的注释,并给出建议
@see 引用:可以添加多个,但必须放在一起
4.9.6 包与概述注释 包注释的两种方式(在包目录添加一个单独的文件):
提供一个 package.html。在标记 <body></body> 之间的所有文本都会抽取出来。
提供一个 package-info.java 文件。文件开头即写文档注释后面是 package 语句。
概述:
创建一个名为 overview.html 文件,这个文件位于所有源文件的父目录中。标记 <body></body> 之间的所有文本都会抽取出来。
4.9.7 注释抽取 1 2 3 4 5 javadoc -d docDirectory nameOfPackage javadoc -d docDirectory nameOfPackage1 nameOfPackage2 javadoc -d docDirectory *.java
4.10 类设计技巧
一定要保证数据私有
一定要对数据初始化,手动初始化
不要在类中使用过多的基本类型
不是所有的域都需要独立的域访问器和域更改器
将职责过多的类进行分解
类名和方法名能够体现它们的职责
优先使用不可变的类
5 继承 5.1 类、超类和子类 5.1.1 定义子类 关键字 extends 表示继承,超类和子类是 Java 程序员最常用的两个术语。子类比超类拥有更多的功能。将通用方法放在超类中,而将特殊扩展方法放在子类中。
1 2 3 public class Manager extends Employee { }
5.1.2 覆盖方法 子类可覆盖超类的方法(同名,同参数),子类不能直接访问超类的私有域,可通过 super 关键字调用父类的方法。super 不是一个对象的引用,只是指示编译器调用超类方法的特殊关键字。
5.1.3 子类构造器 使用 super 调用超类构造器的语句必须放在子类构造器的第一条语句,如果超类没有不带参数的构造器,子类又没有显示调用超类的其他构造器,编译器则会报错。
一个对象变量可以指示多个实际类型的现象被称为多态,在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
5.1.4 继承层次 由一个公共超类派生出来的所有类的集合称为继承层次,在继承层次中,某个特定类到其祖先的路径称为该类的继承链。Java 不支持多继承。
5.1.5 多态 继承关系 is-a 的另一表述是置换法则,程序中出现超类对象的任何地方都可以用子类对象置换。Java 中,对象变量是多态的。子类数组的引用可以转换成超类数组的引用,而无需强制类型转换。
如果方法或构造器由 private 或 static 或 final 修饰,那么编译器能准确知道调用哪个方法,这种调用方式称为静态绑定。由于动态绑定的机制,运行时,调用方法先查询当前类对象的方法,然后查询所继承超类的方法。
5.1.6 final 类和方法 用 final 修饰的类无法被继承,用 final 修饰的方法,子类无法覆盖。声明为 final 的主要目的是确保它们在子类中不会改变语义。
5.1.7 强制类型转换
只能在继承层次内进行类型转换
在将超类转换成子类之前,应该使用 instanceof 进行检查
一般情况下应该尽量少用类型转换和 instanceof 运算符
5.1.8 抽象类 使用关键字 abstract 声明一个抽象类和抽象方法。为了程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法外,抽象类还可以包含字段和具体方法。
类即使不含抽象方法,也能声明为抽象类
抽象类不能实例化
可定义抽象类的对象变量指向非抽象子类的对象
扩展抽象类的两种选择:
子类中任由部分抽象方法,即子类仍为抽象类
子类定义全部的抽象方法,子类不再是抽象类
5.1.9 受保护访问 超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,可以将方法或域声明为 protected。
private —— 仅对本类可见
public —— 对所有类可见
protected —— 对本包和所有子类可见
默认 —— 对本包可见
5.2 Object Object 是 Java 中所有类的超类。
5.2.1 equals 方法 equals 方法是用于检测一个对象是否等于另一个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class Employee { ... public boolean equals (Object otherObject) { if (this == otherObject){ return true ; } if (otherObject == null ){ return false ; } if (getClass() != otherObject.getClass()){ return false ; } Employee other = (Employee)otherObject; return Objects.euqals(name, other.name). && salary == other.salary && Objects.equals(hireDay, other.hireDay); } }public class Manager extends Employee { ... public boolean equals (Object otherObject) { if (!super .equals(otherObject)){ return false ; } Manager other = (Manager)otherObject; return bonus == other.bonus; } }
Java 语言规范要求 equals 方法具有以下的特性:
自反性:对于任意非空引用 x,x.equals(x) 返回 true
对称性:对于任何引用 x、y,y.equals(x) 返回 true,x.equals(y) 也要返回 true
传递性:对于任何引用 x、y、z,x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 也要返回 true
一致性:
对于任意非空引用 x, x.equals(null) 返回 false
编写一个完美的 equals 方法的建议:
显式参数命名为 otherObject
检测 this 与 otherObject 是否引用同一个对象
if(this == otherObject) return true;
检测 otherObject 是否为 null
if(otherObject == null) return false;
比较 this 与 otherObject 是否是同一个类
如果 equals 的语义在每个子类都有改变,使用 getClass 检测
if(getClass() != otherObject.getClass() return false;
如果所有子类都使用统一的语义,就是用 instanceof 检测
if(!(otherObject instanceof ClassName)) return false;
将 otherObject 转换为相应的类类型变量
ClassName other = (ClassName)otherObject;
将所需要比较的域进行比较,基础类型使用 ==,对象引用使用 Objects.equals()
return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;
子类如果重新定义 equals 方法,就要先调用 super.equals(OtherObject) 检测
5.2.2 hashCode 方法 散列码是由对象导出的一个整型值。如果重新定义了 equals 方法,就必须重新定义 hashCode 方法,以便将对象插入到散列表中。
5.2.3 toString 方法 它用于返回对象值的字符串。只要对象与一个字符串通过操作符 + 连接起来,Java 编译器就自动调用 toString 方法。Object 类定义的 toString 方法,用来打印对象所属的类名和散列码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package equals;import java.time.*;import java.util.Objects;public class Employee { private String name; private double salary; private LocalDate hireDay; public Employee (String name, double salary, int year, int month, int day) { this .name = name; this .salary = salary; hireDay = LocalDate.of(year, month, day); } public String getName () { return name; } public double getSalary () { return salary; } public LocalDate getHireDya () { return hireDay; } public void raiseSalary (double byPercent) { double raise = salary * byPercent / 100 ; salary += raise; } public boolean equals (Object otherObject) { if (this == otherObject){ return true ; } if (otherObject == null ){ return false ; } if (getClass() != otherObject.getClass()){ return false ; } Employee other = (Employee)otherObject; return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay); } public int hashCode () { return Objects.hash(name, salary, hireDay); } public String toString () { return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package equals;public 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 bonus) { this .bonus = bonus; } public boolean equals (Object otherObject) { if (!(super .equals(otherObject))){ return false ; } Manager other = (Manager)otherObject; return bonus == other.bonus; } public int hashCode () { return super .hashCode() + 17 * Double.hashCode(bonus); } public String toString () { return super .toString() + "[bonus=" + bonus + "]" ; } }
5.3 泛型数组列表 ArrayList 是一个采用类型参数的泛型类,为了指定数组列表中保存的类型需要使用一对尖括号将类名括起来加在后面,例如 ArrayList<Employee>
。ArrayList 是一个动态扩容数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.util.*;public class ArrayListTest { public static void main (String[] args) { ArrayList<Employee> staff = new ArrayList <>(); staff.add(new Employee ("Carl" , 75000 , 1987 , 12 , 15 )); staff.add(new Employee ("Harry" , 50000 , 1987 , 12 , 15 )); staff.add(new Employee ("Tony" , 40000 , 1990 , 3 , 15 )); for (Employee e : staff){ System.out.println(e); } } }
5.4 对象包装器 对象包装器是不可变的,一旦创建就无法改变其中的值,而且对象包装类是 final,不允许有子类。Integer 类对应的基本类型是 int,尖括号中的类型参数不允许是基本类型。
1 2 3 4 5 6 7 ArrayList<Integer> list = new ArrayList <>(); list.add(3 );int n = list.get(i)
自动装箱规范要求 boolean、byte、char <= 127,介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中(常量池?)。
装箱和拆箱是编译器要做的事情,而不是虚拟机。
5.5 可变数量参数 Object... 与 Object[] 完全一样
,因此 main 方法可以改写为public static void main(String... args)
5.6 枚举类 在比较枚举类型的值时,永远不需要调用 equals,直接使用 == 即可。枚举类型中可以添加构造器、方法和域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class EnumTest { public static void main (String... args) { Size size = Enum.valueOf(Size.class, "SMALL" ); System.out.println(size); System.out.println(size.getAbbreviation()); System.out.println(size == Size.SMALL); } }enum Size { SMALL("S" ), MEDIUM("M" ), LARGE("L" ); private Size (String abbreviation) { this .abbreviation = abbreviation; } public String getAbbreviation () { return abbreviation; } private String abbreviation; }
5.7 反射 能够分析类能力的程序称为反射,反射机制可以用来:
在运行时分析类的能力
在运行时查看对象
实现通用数组操作代码
利用 Method 对象
5.7.1 Class 类 在运行期间,Java 运行时系统始终为所有对象维护一个称为运行时的类型标识,保存这个这些信息的类被称为 CLass。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.*;public class ClassTest { public static void main (String[] args) throws Exception{ Integer i = 10 ; Class cl1 = i.getClass(); String className = "java.lang.Integer" ; Class cl2 = Class.forName(className); Class cl3 = Integer.class; System.out.println(cl1); System.out.println(cl2); System.out.println(cl3); } }
5.7.2 分析类的能力 java.lang.relect 包中有三个类 Field、Method 和 Constructor 分别用于描述类的字段、方法和构造器。
访问私有字段、方法、构造器时,需要设置 setAccessible 为 true。
5.7.3 反射使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Class clazz = Class.forName("top.reajason.test.Student" );Constructor constructor1 = clazz.getConstructor(); System.out.println(constructor1);Constructor constructor2 = clazz.getConstructor(String.class, int .class); System.out.println(constructor2); Constructor[] constructors = clazz.getConstructors();for (Constructor constructor : constructors) { System.out.println(constructor); }Student s1 = (Student) constructor1.newInstance();Student s2 = (Student) constructor2.newInstance("你好" , 13 ); System.out.println(s2); constructor.setAccessible(true );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Class clazz = Class.forName("top.reajason.test.Student" );Constructor constructor = clazz.getConstructor(String.class, int .class);Student s1 = (Student) constructor.newInstance("xiaobai" , 23 );Field name = clazz.getDeclaredField("name" ); name.setAccessible(true ); System.out.println(name.get(s1)); name.set(s1, "nitama" ); System.out.println(name.get(s1));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Class clazz = Class.forName("top.reajason.test.Student" );Student s1 = (Student) clazz.getConstructor(String.class, int .class).newInstance("xioabai" , 13 );Method method = clazz.getMethod("getAge" );Object result = method.invoke(s1); System.out.println(result);
5.8 继承技巧
将公共操作和域放在超类
不要使用受保护的域
使用继承实现 is-a 关系
除非所有继承的方法都有意义,否则不要使用继承
在覆盖方法时,不要改变预期的行为
使用多态而非类型信息
不要过多的使用反射
6 接口、lambda 表达式、内部类 6.1 接口 6.1.1 接口概念 接口不是类,而是对类的一组需求的描述。接口的所有方法默认是 public 而无需指定。接口中不能有实例域,Java SE8 之前不能在接口中实现方法。
类实现一个接口的两个步骤:
让类声明为实现给定的接口,使用 implements
对接口中的所有方法进行定义,实现接口时必须声明为 public
6.1.2 接口的特性 接口不是类,不能使用 new 实例化一个接口,但是可以声明接口变量,指向实现了接口的类对象,也可以使用 instanceof 检测一个对象是否实现了某个接口,接口可以扩展接口,接口中不能包含实例域或静态方法,但是可以包含常量 public static final,一个类只能拥有一个超类,但是可以实现多个接口。
Java SE 8 中,允许接口中增加静态方法,通常放在伴随类中
可以使用 default 声明默认方法,提供默认实现,主要用来接口演化升级,默认方法冲突的两种情况:
一个类实现了多个接口,并且多个接口有共同方法,此时需要类自己实现这个方法,解决冲突
一个类继承的超类和实现的接口中有重名方法,类优先原则,会自动忽略接口的方法。
6.2 接口示例 6.2.1 Comparator 接口 对一个对象数组进行排序的前提是这个对象必须是 Comparable 接口的类的实例。Arrays.sort 方法有两个版本一个是传入单个数组,一个是数组加一个比较器,比较器就是实现了 Comparator 接口的类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.util.*;public class CompareStringTest { public static void main (String[] args) { String[] friends = {"Peter" , "Paul" , "Mary" , "ReaJason" , "Silly" }; Arrays.sort(friends); System.out.println(Arrays.toString(friends)); Arrays.sort(friends, new LengthComparator ()); System.out.println(Arrays.toString(friends)); } }class LengthComparator implements Comparator <String>{ public int compare (String first, String second) { return first.length() - second.length(); } }
6.2.2 Cloneable 接口 Cloneable 接口是 Java 提供的一组标记接口之一,Object 的 clone 方法是 protected,因此支只支持子类调用 clone 方法克隆它自己的对象,必须重新定义为 clone 为 public 才能允许所有方法克隆对象。
实现 Clonable 接口
重新定义 clone 方法,并指定 public 访问修饰符
深拷贝需要拷贝可变实例域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Employee implements Cloneable { private String name; private double salary; private Date hireDay; ... public Employee clone () throws CloneNotSupportedException{ Employee cloned = (Employee)super .clone(); cloned.hireDay = (Date)hireDay.clone(); return cloned; } }
6.3 lambda 表达式 lambda 表达式是一个可传递的代码块,之后可以执行一次或多次。
6.3.1 lambda 表达式语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (String first, String second) -> first.length() - second.length() (String first, String second) -> { if (first.length() < second.length()){ return -1 ; }else if (first.length() > second.length()){ return 1 ; }else { return 0 ; } } () -> { for (int i = 0 ; i < 10 ; i++){ System.out.println(i); } };
6.3.2 函数式接口 对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式,这种接口称为函数式接口。
1 2 3 4 5 6 7 Arrays.sort(words, (first, second) -> { first.length() - second.length() }); list.removeIf(e -> e==null );
6.3.3 方法引用
object::instanceMethod
Class::staticMethod
Class::instanceMethod
System.out::println
等价于 x -> System.out.println(x)
String::compareToIgnoreCase
等价于 (x, y) -> x.compareToIgnoreCase(y)
this\super:this::equals、super::greet
构造器引用:Person::new、Person[]::new
6.3.6 变量作用域 lambda 表达式看可以捕获外围作用域中的变量,且是最终变量(final,初始化之后不会再赋给新值),不过只能引用而不能修改。
6.3.7 Comparator Comparator 接口中包含了静态方法创建比较器,camparing 方法即是一个键提取器函数。p242
1 2 3 4 5 6 7 8 9 10 11 Arrays.sort(people, Comparator.comparing(Person::getName)); Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName()); Arrays.sort(people, Comparator.compringInt(p -> p.getName().length())); Arrays.sort(people, compring(Person::getMiddleName(), nullFirst(naturalOrder()));
6.4 内部类 内部类是定义在另一个类中的类
内部类方法可以访问该类定义所在的作用域中的数据
内部类可以对同一个包中的其他类隐藏起来
当想要定义一个回调函数且不想编写大量代码就可以使用匿名内部类
内部类中声明的所有静态域必须是 final,内部类不能有 static 方法(可以有但是不要写)
6.4.1 内部类的特殊语法 OuterClass.this 表示外围类的引用
OuterClass.InnerClass 在外围类作用于之外,引用内部类
6.4.2 局部内部类 在方法中定义局部内部类,局部内部类不能用 public 或 private 修饰,它的作用域被限定在这个局部内部类所在的块中。局部内部类访问局部变量必须是 final。
6.4.3 匿名内部类 没有名字的内部类,如果构造参数的小括号跟一个大括号,正在定义的就是匿名内部类
1 2 3 4 new SuperType (construction parameters){ inner class methods and data }
6.4.4 静态内部类 静态内部类不能访问外围类对象数据,静态内部类可以有静态域和方法,内部类不需要访问外围类对象的时候应该使用静态内部类。
6.5 代理 利用代理可以在运行时创建一个实现了一组给定接口的新类。
7 异常、断言、日志 7.1 处理错误 7.1.1 异常分类 所有异常都是由 Throwable 继承而来,又分为 Error 和 Exception。Error 描述了 Java 运行时系统的内部错误和资源耗尽错误。Exception 中分为 RuntimeException 和 其他异常 两个分支。Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受查异常,所有其他的异常称为受查异常。编译器将核查是否为所有的受查异常提供了异常处理器。
7.1.2 声明受查异常 下面四个情况应该抛出异常:
调用一个抛出受查异常的方法
程序运行过程中发现错误,使用 throw 抛出
程序出现错误
Java 虚拟机和运行库出现的内部错误
一个方法有可能抛出多个受查异常类型,就需要在方法的首部使用 throws 列出所有的异常类,不应该声明从 RuntimeException 继承的非受查异常。
子类方法中应该比超类方法抛出更特定的异常,或者根本不抛出异常。如果超类没有抛出受查异常,子类也不能抛出受查异常。
7.1.3 创建异常类
继承一个异常类
编写一个构造器方法和一个带有详细描述信息的构造器
1 2 3 4 5 6 class FileFormatException extends IOException { public FileFormatException () {} public FileFormatException (String msg) { super (msg); } }
7.2 捕获异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 try { code }catch (ExceptionType e){ handler for this type }try { code }catch (ExceptionType1 e){ handler for this type }catch (ExceptionType2 e){ handler for this type }try { code }catch (ExceptionType1 | ExceptionType2 e){ handler for this type }
7.2.1 再次抛出异常 1 2 3 4 5 6 7 8 catch (SQLException e){ Throwable se = new ServletException ("database error" ); se.iniiCase(e); throw se; }Throwable e = se.getCause()
7.2.2 finally try 语句可以只有 finall 子句而没有 catch 语句, finnal 语句无论是否遇到异常都会执行,通常用于资源关闭。当 try 和 finally 中有 return 语句时,会走 finally 子句。
强烈建议使用 try/catch 和 try/finally 语句块
1 2 3 4 5 6 7 8 9 10 try { try { }finally { } }catch (){ }
7.2.3 try-with-resource 1 2 3 4 try (Resource res = ...){ }
7.2.4 Throwable
7.3 使用异常机制技巧
异常处理不能代替简单的测试
不要过分细化异常
利用异常层次结构
不要压制异常
检查错误时,苛刻要比放任更好
不要羞于传递异常
7.4 断言 确信某个属性符合要求,并且代码的执行依赖这个属性,语法为 assert 条件
和 assert 条件 : 表达式
,表达式的目的时产生一个消息字符串。默认情况下,断言是被禁用的。
开启断言,-enableassertions 或 -ea
关闭断言,-disablesssertions 或 -da
断言只用于开发和测试阶段。
7.5 日志 7.5.1 日志对象 1 2 3 4 5 Logger.getGlobal().info("" );private static final Logger myLogger = Logger.getLogger("top.reajason.corejava" )
与包名类似,日志记录器名也具有层次结构,子记录器会继承父记录器的级别。7 个日志记录器级别如下:
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
1 2 3 4 5 6 7 logger.setLevel(Level.FINE); logger.waring(message); logger.info(message); logger.log(Level.FINE, message);
7.5.2 日志管理器配置 默认情况下,配置位于:jre/lib/logging.prperties
7.5.3 处理器 默认是 ConsoleHandler 控制台处理器
1 2 3 FileHandler handler = new FileHnadler (); logger.addHandler(handler);
自定义处理器需要扩展 Handler 类,并实现 publish、flush 和 close 方法。
7.5.4 过滤器 同一时刻只能有一个过滤器,通过实现 Fileter 接口并定义 isLoggable 方法自定义过滤器,使用 setFilter 方法添加过滤器。
7.5.5 格式化器 扩展 Formatter 类并实现 format 方法,进行格式化,使用 setFormatter 方法加入到处理器中。
7.6 调试技巧
打印或记录任意变量值
类中加入 main 方法进行单元测试
使用 JUnit 进行测试
日志代理
利用 Throwable 类提供的 printStackTrace 方法,打印堆栈情况,并重新抛出异常。
堆栈轨迹显示在 System.err 上,将错误信息保存在文件中
查看类的加载过程,使用 -verbose 启动虚拟机
-Xlint 告诉编译器对普遍容易出现的代码问题进行检查
jconsole processID 可以监控和管理程序
jmap 可以获得堆的转储
-Xprof 标志运行虚拟机,就会将进场被调用的方法的剖析信息发送到 System.out 中
8 泛型程序设计 泛型程序设计意味着编写的代码可以被很多类型的对象所重用。泛型提供了类型参数,使程序具有更好的可读性和安全性。
8.1 定义简单泛型类 泛型类就是具有一个或多个类型变量的类。
Java 中,E 表示集合的元素类型;K 和 V 分别表示表的关键字和值的类型;T(U 或 S)表示 “任意类型”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Pair <T>{ private T first; private T second; public Pair () { first = null ; second = null ; } public Pair (T first, T second) { this .first = first; this .second = second; } public T getFirst () { return first; } public T getSecond () { return second; } public void setFirst (T value) { first = value; } public void setSecond (T value) { second = value; } }
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型,可用具体的类型替换类型变量就可以实例化泛型类型。
8.2 泛型方法 类型变量放在修饰符之后,返回值类型前,泛型方法可定义在普通类也可定义在泛型类中。
1 2 3 4 5 6 7 8 9 10 11 class ArrayAlg { public static <T> T getMiddle (T... a) { return a[a.length / 2 ]; } } ArrayAlg.<String>getMiddle("John" , "Q" , "Public" ); ArrayAlg.getMiddle("John" , "Q" , "Public" );
8.3 类型变量的限定 <T extends BoundingType>
表示 T 应该是绑定类型的子类型,T 和绑定类型可以是类也可以是接口。一个类型变量或通配符可有多个限定,使用 & 分隔,限定中至多一个类,且类要放在第一个。
1 2 3 4 5 6 class ArrayAlg { public static <T extends Comparable > Pair<T> minmax (T[] a) { ... return new Pair <>(a[0 ], a[a.length - 1 ]); } }
8.4 泛型代码与虚拟机 虚拟机没有泛型类型对象,所有对象都是普通类。
8.4.1 类型擦除 泛型类型都会自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型,替换为 Object)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Pair { private Object first; private Object second; public Pair () { first = null ; second = null ; } public Pair (Object first, Object second) { this .first = first; this .second = second; } public Object getFirst () { return first; } public Object getSecond () { return second; } public void setFirst (Object value) { first = value; } public void setSecond (Object value) { second = value; } }
8.4.2 翻译泛型表达式 调用泛型方法或者存取泛型域时,编译器会自动插入强制类型转换。
8.4.3 翻译泛型方法 子类继承泛型类并且子类重写泛型类中的泛型方法制定了确定类型,为了防止类型擦除与多态发生冲突,编译器会在子类生成一个桥方法,虚拟机运行时会调用子类桥方法。
虚拟机没有泛型,只有普通的类和方法
所有参数类型都用它们的限定类型替换
桥方法被合成用来保持多态
为保持类型安全,必要时插入强制类型转换
8.4.4 调用遗留代码 设计泛型类型时,主要目标是运行泛型代码和遗留代码互操作。
@SuppressWarning("unchecked")
可以抑制警告
8.5 约束和局限性 8.5.1 不能用基本类型实例化类型参数 原因是类型擦除
8.5.2 运行时类型查询只适用于原始类型 虚拟机中的对象总是一个特定的非泛型方法,所以类型查询只产生原始类型。
8.5.3 不能创建参数化类型数组 类型擦除,会使 Pair<String>[] table
变成 Pair[] table
,可以声明但是使用会有问题,会得到一个警告,可以使用注解抑制警告 @SuppressWarning("unchecked")
或 @SafeVarargs
8.5.4 不能实例化类型变量 new T() 不能使用,因为类型擦除,T 变成 Object 了,可以使用构造器表达式解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static <T> Pair<T> makePair (Supplier<T> constr) { return new Pair <>(constr.get, constr.get()); } Pair<String> p = Pair.makePair(String::new );public static <T> Pair<T> makePair (Class<T> cl) { try { return new Pair <>(cl.newInstance(), cl.newInstance()); }catch (Exception e){ return null ; } } Pair<String> p = Pair.makePair(String.class);
8.5.5 不能构造泛型数组 8.5.6 泛型类的静态上下文中类型变量无效 8.5.7 不能抛出或捕获泛型类的实例 8.5.8 可以消除对受查异常的检查 8.5.9 注意擦除后的冲突 8.6 泛型类型的继承规则 Pair<Manager>
和 Pair<Employee>
没有任何关系。可以将参数化类型转换为原始类型,泛型类可以扩展或实现其他的泛型类。
8.7 通配符类型 通配符类型,允许类型参数变化。Pair<? extends Employee>
表示类型参数是 Employee 的子类。Pair<? super Manager>
表示类型参数是 Manager 的超类。
?
表示无限定通配符。
9 集合 9.1 集合框架 集合接口与实现分离,集合有两个基本接口 Collection 和 Map。
9.1.1 Collection 接口
9.1.2 Iterator 迭代器认为是位于两个元素之间,remove 只能删除上次访问的元素,不能连续调用两次
9.2 具体的集合
集合类型
描述
ArrayList
一种可以动态增长和缩减的索引序列
LinkedList
一种可以在任何位置进行搞笑地插入和删除操作的有序序列
ArrayDeque
一种用循环数组实现的双端队列
HashSet
一种没有重复元素的无序集合
TreeSet
一种有序集
EnumSet
一种包含枚举类型的值
LinkedHashSet
一种可以记住元素插入次序的集
PriorityQueue
一种允许高效删除最小元素的集合
HashMap
一种存储键值关联的数据结构
TreeMap
一种键值有序排列的映射表
EnumMap
一种键值属于枚举类型的映射表
LinkedHashMap
一种可以记住键值项添加次序的映射表
WeakHashMap
一种其值无用武之地后可以被垃圾回收回收的映射表
IdentityHashMap
一种用 == 而不是用 equals 比较键值的映射表
9.2.1 链表 插入和删除操作高效,ListIterator 继承于 Iterator 支持添加、修改值和反向遍历。Java 设计上不合理,不要使用 get 获取链表上的元素,每次都需要从头遍历,应该使用迭代器。LinkedList 继承于 List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
9.2.2 数组链表 ArrayList 继承于 List,可以随机遍历数组
9.2.3 散列集 HashSet 继承于 Set,没有重复元素的集合
9.2.4 树集 TreeSet 有序集合,排序使用的红黑树结构,要使用树集,元素必须实现 Comparable 接口,或构造集时提供 Comparator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
9.2.5 队列 双端队列,高效地在头部和尾部同时进行添加或删除元素,不支持在队列中间添加元素,ArrayDeque 和 LinkedList 有实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
9.2.6 优先级队列 堆结构,小根堆,大根堆
9.3 映射 HashMap 和 TreeMap 都实现了 Map 接口。键必须是唯一的
9.3.1 基本映射操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
9.3.2 更新映射项
9.3.3 映射视图 映射的视图(实现了 Collection 接口或某个子接口的对象)有三种:键集、值集合以及键值对集。
1 2 3 Set<K> keySet () ; Collection<V> values () ; Set<Map.Entry<K,V>> entrySet();
9.3.4 弱散列映射 WeakHashMap,当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同删除键值对。使用弱引用保存键。
9.3.5 链接散列集与映射 LinkedHashSet 和 LinkedHashMap 能记住插入元素的顺序。
9.3.6 枚举集与映射 EnumSet内部用位序列实现。EnumMap 是一个键类型为枚举类型的映射
9.3.7 标识散列映射 IdentityHashMap 使用 == 而不是 equals 比较两个对象
9.4 视图与包装类 keySet 方法返回一个实现 Set 接口的类对象,这个类的方法对原映射进行操作,这种集合称为视图。
9.4.1 轻量集合包装器
9.4.2 子范围
9.4.3 不可修改的视图 Collections 有方法获取集合不可修改视图,如果尝试修改则抛出异常。
9.4.4 同步视图 使用视图机制确保常规集合的线程安全。
9.4.5 受查视图 受查视图可以探测到集合不能探测到的代码问题,受查视图受限于虚拟机可以运行的运行时检查
9.5 算法 9.5.1 排序与混排 Arrays.sort()
9.5.2 二分查找 Collections.binarySearch()
9.5.3 Collections 其他
9.5.4 集合与数组转换 数组转集合,Arrays.asList()
集合转数组,list.toArray() 返回 Object[],转为特定类型需要使用 list.toArray(new String[0]) 或 list.toArray(new String[list.size()]) 这种不会创建新数组
9.5.5 编写自己的算法 集合声明时应该尽可能使用接口而非具体的实现,返回集合的方法,可能还要返回接口,而不是返回类。
9.6 遗留的集合 9.6.1 Hashtable Hashtable 与 HashMap 作用一样
9.6.2 枚举 hasMoreElements 和 nextElement 与迭代器的 hasNext 和 next 方法相似。
9.6.3 属性映射
键值都是字符串
表可以保存到文件,也可以从文件加载
使用一个默认的辅助表
9.6.4 栈
9.6.5 位集 BitSet 存放一个位序列,高效
13 部署 Java 应用程序 13.1 JAR 文件 13.1.1 创建 JAR 文件 jar cvf JARFileName File1 File2
13.1.2 清单文件 jar cfm JARFileName MainifestFileName ...
13.1.3 可执行 JAR 使用 e 指定程序入口,或在清单中国指定
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass ...
启动 jar:jar -jar MyProgram.jar
13.1.4 资源 文件的自动装载是利用资源加载特性完成的。
13.1.5 密封 在清单中加入 Sealed: true
则指定密封
13.2 应用首选项的存储 13.2.1 属性映射 使用 properties 存储属性,获取主目录:System.getProperties("user.home")
13.2.2 首选项 API Preferences