Java学习笔记
Java快速入门
Java的数据类型
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
数组排序
数组包:import java.util.Arrays
可自己实现各种排序。
也可直接调用JDK提供的方法。
升序排序
Arrays.sort()
降序排序
利用Collections.reverseOrder()方法
需要import java.util.Collections
核心语句:Arrays.sort(a, Collections.reverseOrder());
示例代码:
import java.util.Arrays;
import java.util.Collections;
public class myTest {
public static void main(String [] args) {
Integer [] a = {28, 12, 89, 73, 65, 18, 96, 50, 8, 36};
Arrays.sort(a, Collections.reverseOrder());
for (int arr: a) {
System.out.print(arr + " ");
}
}
}
重载Comparator接口的compare()方法
需要import java.util.Comparator
核心代码:
Arrays.sort(a, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
示例代码:
import java.util.Arrays;
import java.util.Comparator;
public class myTest {
public static void main(String [] args) {
Integer [] a = {28, 12, 89, 73, 65, 18, 96, 50, 8, 36};
Arrays.sort(a, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for (int arr: a) {
System.out.print(arr + " ");
}
}
}
面向对象编程之面向对象基础
继承
关键字
extends
继承super
父类,子类引用父类的字段时,可以用super.fieldName
转型
例如,Person为父类,Student为子类
向上转型(upcasting)
由于Student继承自Person,Person继承自Object。子类拥有父类的全部功能。
Person类型的变量,如果指向Student类的实例,对它进行操作,是没有问题的。
也就是说,父类类型的变量指向子类类型的实例,进行操作,是ok的
Student s = new Student();
Person p = s; // upcasting
Object o1 = p; // upcasting
Object o2 = s; // upcasting
向下转型(downcasting)
把一个父类类型强制类型转换为子类类型
Person p1 = new Student(); // upcasting
Person p2 = new Person();
Student s1 = (Student) p1; //downcasting
Student s2 = (Student) p2; // runtime error! ClassCastException!
实际开发中,强制向下转型时,最好借助instanceof
判断
多态
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法
区分Override和Overload
- 方法签名不同 ——Overload
- 方法签名相同 ——Override
upcasting时方法的调用
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
也就是说,对于Person p = new Student()
,p.run()
调用的是Student类型的run()方法
调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用,例如:
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
final关键字
- 用
final
修饰的方法不能被Override:继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。 - 用
final
修饰的类不能被继承:如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
- 用
final
修饰的字段在初始化后不能被修改:对于一个类的实例字段,同样可以用final
修饰,这样的字段只能在构造方法中初始化。类的实例一旦被创建,该字段不可修改。
抽象类
关键字 abstract class
定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法。
如果不实现抽象方法,则该子类仍是一个抽象类。
接口
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口interface
(关键字)。实现接口使用implements
关键字。
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
接口继承
一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。
default方法
在接口中,可以定义default
方法。
实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
静态字段和静态方法
关键字static
静态字段和静态方法
静态字段:
实例对象能访问静态字段只是因为编译器可以根据实例类型将实例对象.静态字段
自动转换为类名.静态字段
。
推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段(非实例字段)
静态方法:
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。
因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。
通过实例变量也可以调用静态方法,但同样,这也只是编译器自动帮我们把实例改写成类名而已。
静态方法经常用于工具类。例如:Arrays.sort()、Math.random()
静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。
通常情况下,通过实例变量访问静态字段和静态方法,会得到一个编译警告。
接口的静态字段
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型(在定义时赋值)。
实际上,因为interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉。编译器会自动把该字段变为public static final
类型。
包(package)
——相当于C/C++中的namespace
一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名
。
在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用.
隔开。例如:java.util
。
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
按包结构组织文件
没有定义包名的class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
我们还需要按照包结构把Java文件(一般放src目录下)组织起来。编译后的.class
文件(一般放bin目录下)也需要按照包结构存放
包的作用域
位于同一个包的类,可以访问包作用域的字段和方法。
import
。。。。
Java核心类
Java核心类:字符串、StringBuilder、StringJoiner、包装类型、JavaBean、枚举、常用工具类
String类
在Java中,String是一个引用类型,它本身也是一个class。但是,Java编译器对String有特殊处理,即可以直接用"..."
来表示一个字符串
实际上字符串在String内部是通过一个char[]
数组表示的,因此,按下面的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
一些常用方法
字符串比较
必须使用equals()
方法,不能用==
要忽略大小写比较,使用equalsIgnoreCase()
方法
判断是否包含子串
"Hello".contains("ll");//true
注意到contains()
方法的参数是CharSequence
而不是String
(CharSequence
是String
的父类)
更多搜索子串的实例
"Hello".indexOf("1"); //2
"Hello".lastIndexOf("1"); //3
"Hello".startsWith("He"); //true
"Hello".endsWith("lo"); //true
提取子串的例子
"Hello".substring(2); //"llo",2表示startIndex
"Hello".substring(2,4); //"llo",2表示startIndex,4表示endIndex
去除首/尾空白字符
(1)使用trim()
方法,空白字符包括:空格、\t
、\r
、\n
。例如:
" \tHello\r\n ".trim(); //"Hello"
注意:trim()方法没有改变字符串的内容,而是返回了一个新字符串
(2)另一个strip()
方法也可以移除首尾空白字符,与前者不同的是,类似中文的空格字符\u3000
也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
(3)只去前或去后
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
判断字符串是否为空
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
判断字符串是否为空白字符串
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
(1)根据字符或字符串替换
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
(2)通过正则表达式替换
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
分割字符串
split()
方法
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字符串
静态方法join()
,它用指定的字符串连接字符串数组
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法。
//任意基本类型转换为字符串
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
要把字符串转换为其他类型,就需要根据情况。
//把字符串转换为int类型
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
//把字符串转换为boolean类型
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
//String和char[]互相转换
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String,这里如果后面修改了cs的内容,s不会改变
要特别注意,Integer
有个getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:
Integer.getInteger("java.version"); // 版本号,11
字符编码
在Java中,char
类型实际上是两个字节的Unicode
编码。可以手动修改字符编码:
byte[] b1 = "Hello".getBytes(); // 按ISO8859-1编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
注意:转换编码后,就不再是char
类型,而是byte
类型表示的数组
如果要把已知编码的byte[]
转换为String
:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
而Java的String和char在内存中总是以Unicode编码表示。
StringBuilder
在Java中,拼接字符串可以直接用+
。
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
上述代码中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
。它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象。
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
//进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。
sb.append("Mr ").append("Bob").append("!").insert(0, "Hello, ");
System.out.println(sb.toString());
}
}
注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
另:StringBuffer
是StringBuilder
的线程安全版本,现在很少使用。
StringBuilder的构造方法
StringBuilder的常用方法
StringJoiner
满足用分隔符拼接数组的需求:
Output:Hello Bob, Alice, Grace!
使用StringBuilder
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sb = new StringBuilder();
sb.append("Hello ");
for (String name : names) {
sb.append(name).append(", ");
}
// 注意去掉最后的", ":
sb.delete(sb.length() - 2, sb.length());
sb.append("!");
System.out.println(sb.toString());
}
}
使用StringJoiner
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!"); //", " - 分隔符,"Hello " - 前言,"!" - 后话
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
事实上,如果查看源码,可以发现,StringJoiner
内部实际上就是使用了StringBuilder
,所以拼接效率和StringBuilder
几乎是一模一样的。
String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
Java核心库为每种基本类型都提供了对应的包装类型
基本类型 | 对应的引用类型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
使用示例:
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}
自动装箱(Auto Boxing)和自动拆箱(Auto Unboxing)
P.S. 这两者只发生在编译阶段,目的是为了少写代码。
Integer n = 100; // 编译器自动使用Integer.valueOf(int),auto boxing
int x = n; // 编译器自动使用Integer.intValue(),auto unboxing
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
public class Main {
public static void main(String[] args) {
Integer n = null;
int i = n;
}
}
不变类
所有的包装类型都是不变类。查看Integer
的源码可知,
public final class Integer {
private final int value;
}
一旦创建了Integer
对象,该对象就是不变的。
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较
包装类型的实例创建
例如Integer
实例,有以下两种方法
Integer n = new Integer(100);
Integer n = Integer.valueOf(100); //静态工厂方法
后者更好,因为方法1总是创建新的Integer实例,方法2把内部优化留给Integer的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
创建新对象时,优先选用静态工厂方法而不是new操作符。
进制转换
指定进制将字符串解析为一个整数
静态方法parseInt()
可以
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
把整数格式化为指定进制的字符串
public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
}
}
P.S. 注意:上述方法的输出都是String,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100在内存中总是以4字节的二进制表示。
——数据的存储和显示要分离
一些静态变量
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
通过包装类型获取各种基本类型
所有的整数和浮点数的包装类型都继承自Number
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和long
都是带符号整型,最高位是符号位。转换需要借助包装类型的静态方法完成。
例如,byte是有符号整型,范围是-128~+127
,但如果把byte
看作无符号整型,它的范围就是0~255
。我们把一个负的byte
按无符号整型转换为int
:
public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}
因为byte
的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
。
类似地,可以把一个short
按unsigned转换为int
,把一个int
按unsigned转换为long
JavaBean
JavaBean
是一种符合命名规范的class,它通过getter
和setter
来定义属性- 属性是一种通用的叫法,并非Java语法规定
- 可以利用IDE快速生成
getter
和setter
- 使用
Introspector.getBeanInfo()
可以获取属性列表
枚举类 - enum
enum
:为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类
使用示例:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
enum
常量本身带有类型信息,即Weekday.SUN
类型是Weekday
,编译器会自动检查出类型错误。
int day = 1;
if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '=='
}
enum
类型的特点:
- 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承 - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例 - 定义的每个实例都是引用类型的唯一实例
- 可以将
enum
类型用于switch
语句
一些常用方法
name()
- 返回常量名:
String s = Weekday.SUN.name(); // "SUN"
original()
- 返回定义的常量的顺序,从0开始计数:
int n = Weekday.MON.ordinal(); // 1
P.S. 改变枚举常量定义的顺序就会导致ordinal()返回值发生变化。
给枚举常量添加字段
事实上,我们可以为enum编写构造方法、字段和方法,但是,enum的构造方法要声明为private,字段强烈建议声明为final
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
此外,默认情况下,对枚举常量调用toString()
会返回和name()
一样的字符串。但是,toString()
可以被覆写,而name()
则不行。我们可以给Weekday
添加toString()
方法从而使输出更具有可读性。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
@Override
public String toString() {
return this.chinese;
}
}
BigInteger
在Java中,由CPU原生提供的整型最大范围是64位long型整数。更大的可以使用java.math.BigInteger
(可以表示任意大小的整数)。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数。
BigInteger
是不变类,并且继承自Number
,做运算时只能用实例方法,例如add()
。
将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。
BigDecimal
——与BigInteger
类似,表示一个任意大小且精度完全准确的浮点数。
scale()
方法可以返回小数位数。
stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0
的BigDecimal
。
比较BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
。
Java常用类
Arrays
Calendar
Calendar
类是一个抽象类,它完成Date
类与普通日期表示法之间的转换,而我们更多的是使用Calendar
类的子类GregorianCalendar
类。它实现了世界上普遍使用的公历系统。当然我们也可以继承Calendar
类,然后自己定义实现日历方法。
GregorianCalendar 类的构造函数
Date
不过,Date
类的很多方法自JDK1.1开始就已经过时了
Math
Math
类在java.lang
包中,包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。
Random
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。缺省则使用系统当前时间戳作为种子。
要生成一个随机数,可以使用nextInt()
、nextLong()
、nextFloat()
、nextDouble()
:
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
SecureRandom
——通过操作系统提供的安全的随机种子来生成随机数,无法手动指定。
System
异常处理
Java的异常
所谓错误,就是程序调用某个函数的时候,如果失败了,就表示出错。
调用方如何获知调用失败的信息?有两种方法:
- 约定返回错误码:常见底层C函数
- 在语言层面上提供一个异常处理机制:Java内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种class
,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了。
因为Java的异常是class
,它的继承关系如下:
Error
表示严重的错误,程序对此一般无能为力。而Exception
则是运行时的错误,它可以被捕获并处理。
Error
例如:
OutOfMemoryError
:内存耗尽NoClassDefFoundError
:无法加载某个ClassStackOverflowError
:栈溢出
Exception
分为两大类:
RuntimeException
以及它的子类- 非
RuntimeException
(包括IOException
、ReflectiveOperationException
等等)
Java规定:
- 必须捕获的异常,包括
Exception
及其子类,但不包括RuntimeException
及其子类,这种类型的异常称为Checked Exception - 不需要捕获的异常,包括
Error
及其子类,RuntimeException
及其子类
捕获异常
捕获异常使用try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类。
在方法定义时,使用throws Xxx
表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,或者用throws
声明,否则编译器会报错。
只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()
方法中捕获,不会出现漏写try
的情况。这是由编译器保证的。main()
方法也是最后捕获Exception
的机会。
如果不想写任何try
代码,可以直接把main()
方法定义为throws Exception
。因为main()
方法声明了可能抛出Exception
,也就声明了可能抛出所有的Exception
,因此在内部就无需捕获了。代价是一旦发生异常,程序会立刻退出。
捕获异常后,即使什么也处理不了,也应把异常记录下来。
所有异常都可以调用printStackTrace()
方法打印异常栈,这是一个简单有用的快速打印异常的方法。
多catch语句
多个catch语句只有一个能被执行:
可以使用多个catch
语句,每个catch
分别捕获对应的Exception
及其子类。JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。
存在多个catch
的时候,catch
的顺序非常重要:子类必须写在前面,否则它永远不会被捕获到(因为都被父类捕获过去了)。
finally语句
——无论是否有异常发生,都会执行的语句。finally总是最后执行。
如果没有发生异常,就正常执行try { ... }
语句块,然后执行finally
。
如果发生了异常,就中断执行try { ... }
语句块,然后先跳转执行匹配的catch
语句块,最后执行finally
。
捕获多种异常
比如处理IOException
和NumberFormatException
的代码是相同的,我们可以把它俩用|
合并到一起。
也就是说,一个catch
语句也可以匹配多个非继承关系的异常。
try {
...
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
}
抛出异常
——两步走:
- 创建某个
Exception
的实例; - 用
throw
语句抛出。
例如,
void process2(String s) {
if (s==null) {
NullPointerException e = new NullPointerException();
throw e;
}
}
上述代码更常见的写法是:
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
如果一个方法捕获了某个异常后,又在catch
子句中抛出新的异常,就相当于把抛出的异常类型“转换”了。
如果在构造异常的时候,没有把原始的Exception
实例传进去,新的异常会丢失原始异常信息。printStackTrace()
时查看不到原始异常的信息。
或者,也可以使用Trowable.getCause()
方法。如果返回null
,说明已经是“根异常”了。
异常屏蔽
——没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
比如,如果在执行finally
语句时抛出异常,那么catch
语句的异常会被屏蔽掉,因为只能抛出一个异常。
在极少数的情况下,我们需要获知所有的异常。
——如何保存所有的异常信息?方法是先用origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出。
public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
}
}
通过Throwable.getSuppressed()
可以获取所有的Suppressed Exception
。
绝大多数情况下,在finally
中不要抛出异常。因此,我们通常不需要关心Suppressed Exception
Java标准库定义的常用异常
Exception
|++ RuntimeException
| |++ NullPointerException
| |++ IndexOutOfBoundsException
| |++ SecurityException
| |++ IllegalArgumentException
| | |++ NumberFormatException
|++ IOException
| |++ UnsupportedCharsetException
| |++ FileNotFoundException
| |++ SocketException
|++ ParseException
|++ GeneralSecurityException
|++ SQLException
|++ TimeoutException
自定义异常
抛出异常时,尽量复用JDK已定义的异常类型。
不过,在一个大型项目中,可以自定义新的异常类型,但是要注意保持一个合理的异常继承体系,并且应该提供多种构造方法。
一个常见的做法是自定义一个BaseException
作为“根异常”,然后,派生出各种业务类型的异常。
BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException
派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的BaseException
应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
上述构造方法实际上都是原样照抄RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。
P.S. 通过IDE可以根据父类快速生成子类的构造方法。
使用断言(Assertion)
——关键字assert
,是一种调试程序的方式。
断言条件预期为true
,如果计算结果为false
,则断言失败,抛出AssertionError
,程序结束退出(因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。对于可恢复的程序错误应该抛出异常并在上层捕获处理)。
使用assert
语句时,还可以添加一个可选的断言消息,使得断言失败时AssertionError
会带上断言消息,更加便于调试。
assert x >= 0 : "x must >= 0";
注意:JVM默认关闭断言指令,即遇到assert
语句自动忽略,不执行。
要执行assert
语句,必须给Java虚拟机传递-enableassertions
(可简写为-ea
)参数启用断言。
还可以有选择地对特定的类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main
,表示只对com.itranswarp.sample.Main
这个类启用断言。
对特定的包启用断言,命令行参数是:-ea:com.itranswarp.sample...
(注意结尾有3个.
),表示对com.itranswarp.sample
这个包启动断言。
实际开发中,很少使用断言。更好的方法是编写单元测试。
日志
使用JDK Logging
日志(Logging)——取代手动使用System.out.println()
调试。
好处:
- 可以设置输出样式,避免自己每次都写"ERROR: " + var;
- 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
- 可以……
Java标准库内置了日志包java.util.logging
,可以直接使用。
JDK的Logging定了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。
使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
局限:
- Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行
main()
方法,就无法修改配置。 - 配置不太方便,需要在JVM启动时传递参数
-Djava.util.logging.config.file=<config-file-name>
因此,其实Java标准库内置的Logging使用并不是非常广泛。
public class Hello {
public static void main(String[] args) {
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
}
输出结果如下:
Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM Hello main
SEVERE: process will be terminated...
使用Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。它是目前使用最广泛的日志模块。
Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。(也就是说,它可以自动检测并使用其他日志模块)
使用Commons Logging只需要和两个类打交道,并且只有两步:
- 通过
LogFactory
获取Log
类的实例
2.使用Log
实例的方法打日志。
由于Commons Logging是一个第三方提供的库,所以必须先下载(地址:https://commons.apache.org/proper/commons-logging/download_logging.cgi)下来。
下载后,解压,找到commons-logging-1.2.jar
这个文件,再跟需要编译运行的Java源码放到一个目录下,例如:
work
│
├─ commons-logging-1.2.jar
│
└─ Main.java
然后用javac
编译Main.java
,编译的时候要指定classpath
,不然编译器找不到我们引用的org.apache.commons.logging
包。编译命令如下:
javac -cp commons-logging-1.2.jar Main.java
如果编译成功,那么当前目录下就会多出一个Main.class
文件:
work
│
├─ commons-logging-1.2.jar
│
├─ Main.java
│
└─ Main.class
现在可以执行这个Main.class
,使用java
命令,也必须指定classpath
,命令如下:
java -cp .;commons-logging-1.2.jar Main
注意到传入的classpath
有两部分:一个是.
,一个是commons-logging-1.2.jar
,用;
分割。.
表示当前目录,如果没有这个.
,JVM不会在当前目录搜索Main.class
,就会报错。
如果在Linux或macOS下运行,注意classpath
的分隔符不是;
,而是:
。
java -cp .:commons-logging-1.2.jar Main
运行结果如下:
Mar 02, 2019 7:15:31 PM Main main
INFO: start...
Mar 02, 2019 7:15:31 PM Main main
WARNING: end.
Commons Logging定义了6个日志级别:
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO
。
使用Commons Logging时,如果在静态方法中引用Log
,通常直接定义一个静态类型变量。
// 在静态方法中引用Log:
public class Main {
static final Log log = LogFactory.getLog(Main.class);
static void foo() {
log.info("foo");
}
}
在实例方法中引用Log
,通常定义一个实例变量。
// 在实例方法中引用Log:
public class Person {
protected final Log log = LogFactory.getLog(getClass());
void foo() {
log.info("foo");
}
}
注意到实例变量log的获取方式是LogFactory.getLog(getClass())
,虽然也可以用LogFactory.getLog(Person.class)
,但是前一种方式有个非常大的好处,就是子类可以直接使用该log实例。
由于Java类的动态特性,子类获取的log字段实际上相当于LogFactory.getLog(Student.class)
,但却是从父类继承而来,并且无需改动代码。
此外,Commons Logging的日志方法,例如info()
,除了标准的info(String)
外,还提供了一个非常有用的重载方法:info(String, Throwable)
,这使得记录异常更加简单:
try {
...
} catch (Exception e) {
log.error("got exception!", e);
}
使用Log4j
Commons Logging负责充当日志API。而Log4j负责实现日志底层。两者搭配使用
更详细说明参加廖雪峰的官方网站(https://www.liaoxuefeng.com/wiki/1252599548343744/1264739436350112)。
使用SLF4J和Logback
其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
更详细说明参加廖雪峰的官方网站(https://www.liaoxuefeng.com/wiki/1252599548343744/1264739155914176)。
反射(Class 类)
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
事实上,通过反射读写字段是一种非常规方法,它会破坏对象的封装。
除了int
等基本类型外,Java的其他类型全部都是class
(包括interface
),例如:String
、Object
、Runnable
、Exception
,…
Class
类的构造方法是private
,只有JVM能创建Class
实例。
一个Class
实例包含了该class
的所有完整信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。
——这种通过Class
实例获取class
信息的方法称为反射(Reflection)。
——如何获取一个class
的Class
实例?三个方法:
- 直接通过一个
class
的静态变量class
获取:
Class cls = String.class;
- 如果我们有一个实例变量,可以通过该实例变量提供的
getClass()
方法获取
String s = "Hello";
Class cls = s.getClass();
- 如果知道一个
class
的完整类名,可以通过静态方法Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
访问字段(Field对象)
获取字段
Class
类提供了以下几个方法来获取字段:
Field getField(name)
:根据字段名获取某个public的field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field(不包括父类)Field getFields()
:获取所有public的field(包括父类)Field[] getDaclaredFields()
:获取当前类的所有field(不包括父类)
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
getType()
:返回字段类型,也是一个Class
实例,例如,String.class
getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义
例如
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
获取字段值
先拿到字段对应的Field
对象,再通过Field.get(Object)
获取相应的值。
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true); //使得这个字段不论是public还是protected还是private,都可访问。但如果JVM运行期存在SecurityManager则可能会失败
Object value = f.get(p); //如果setAccessible(true),当字段为不可访问时,会抛出一个IllegalAccessException
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
设置字段值
——Field.set(Object, Object)
,其中第一个Object
参数是指定的实例,第二个Object
是待修改
的值
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
调用方法(Method对象)
Java的反射API提供的Method对象封装了方法的所有信息。
- 通过
Class
实例的方法可以获取Method
实例:getMethod()
,getMethods()
,getDeclaaredMethod()
,getDeclareMethods()
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
- 通过
Method
实例可以获取方法信息:getName()
,getReturnType()
,getParameterTypes()
,getModeifiers()
- 通过
Method
实例可以调用某个对象的方法:Object invoke(Object instance, Object...parameters)
,第一个参数是对象实例(如果是静态方法,则传入参数null
),即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}
public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
}
}
- 通过设置
setAccessible(true)
来访问非public
方法 - 通过反射调用方法时,仍然遵循多态原则——即总是调用实际类型的覆写方法(如果存在)
调用构造方法(Constructor对象)
Constructor
对象封装了构造方法的所有信息
- 通过
Class
实例的方法可以获取Constructor
实例:getConstructor()
,getConstructors()
,getDeclaredConstructor()
,getDeclaredConstructors()
- 通过
Constructor
实例可以创建一个实例对象:newInstance(Object...parameters)
- 通过设置
setAccessible(true)
来访问非public
构造方法
注意,Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
获取构造方法后,可以通过反射来创建新的实例——调用Class提供的newInstance()
方法
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
获取继承关系(Class对象)
通过Class
对象可以获取继承关系:
Class getSuperclass()
:获取父类类型(Object
的父类是null
,除Object
外,其他任何非interface
的Class
都必定存在一个父类类型)Class[] getInterfaces()
:获取当前类实现的所有接口(不包括其父类实现的接口类型)。如果一个类没有实现任何interface
,则返回空数组- 通过
Class
对象的isAssignableFrom()
方法可以判断一个向上转型是否可以实现
动态代理(Dynamic Proxy)
——可以在运行期动态创建某个interface
的实例。它通过Proxy
创建代理对象,然后将接口方法“代理”给InvocationHandler
完成
先定义接口,但是并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()
创建一个接口的对象。
这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称之为动态代码。
JDK提供的动态创建接口对象的方式,就叫动态代理。
在运行期动态创建一个interface
实例的方法如下:
- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用 - 通过
Proxy.newProxyInstance()
创建interaface
实例,它需要3个参数:- 使用的
ClassLoader
,通常就是接口类的ClassLoader
- 需要实现的接口数组,至少需要传入一个接口进去
- 用来处理接口方法调用的
InvocationHandler
实例
- 使用的
- 将返回的
Object
强制转型为接口
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
注解(Annotation)
——放在Java源码的类、方法、字段、参数前的一种特殊“注释”。
——用@
标识。
注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。
注解分类
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
Java的注解可以分为三类:
- 由编译器使用的注解:
@Override
:让编译器检查该方法是否正确地实现了重载
@SuppressWarnings
:告诉编译器忽略此处代码产生的警告
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了 - 由工具处理
.class
文件使用的注解。
比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。 - 在程序运行期能够读取的注解。
它们在加载后一直存在于JVM中,这也是最常用的注解。
例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
注解的配置参数
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型
- String
- 枚举类型
- 基本类型、String以及枚举的数组
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为value
的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。(也就是说,如果参数名称是value
,且只有一个参数,那么可以省略参数名称。)
如果只写注解,相当于全部使用默认值。
例如,
public class Hello {
@Check(min=0, max=100, value=55)
public int n;
@Check(value=99)
public int p;
@Check(99) // @Check(value=99)
public int x;
@Check
public int y;
}
@Check
就是一个注解。第一个@Check(min=0, max=100, value=55)
明确定义了三个参数,第二个@Check(value=99)
只定义了一个value
参数,它实际上和@Check(99)
是完全一样的。最后一个@Check
表示所有参数都使用默认值。
定义注解
——可以修饰其他注解的注解。
Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写注解。
元注解
@Target
——最常用的元注解。使用@Target
可以定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:
ElementType.TYPE
- 字段:
ElementType.FIELD
- 方法:
ElementType.METHOD
- 构造方法:
ElementType.CONSTRUCTOR
- 方法参数:
ElementType.PARAMETER
实际上@Target
定义的value
是ElementType[]
数组,只有一个元素时,可以省略数组的写法。
例如:定义注解@Report
用在方法上,我们必须添加一个@Target(ElementType.METHOD)
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
再如,定义注解@Report
可用在方法或字段上,可以把@Target
注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }
@Retention
——定义了Annotation
的生命周期:
- 仅编译期:
RetentionPolicy.SOURCE
- 仅class文件:
RetentionPolicy.CLASS
- 运行期:
RetentionPolicy.RUNTIME
如果@Retention
不存在,则该Annotation
默认为CLASS
。
如何使用注解完全由工具决定。SOURCE
类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS
类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME
类型的注解不但要使用,还经常需要编写。
@Repeatable
——定义Annotation
是否可重复。这个注解应用不是特别广泛
经过@Repeatable
修饰后,在某个类型声明处,就可以添加多个@Report
注解
@Inherited
——定义子类是否可继承父类定义的Annotation
。它仅针对@Target(ElementType.TYPE)
类型的Annotation
有效,并且仅针对class
的继承,对interface
的继承无效
如何定义注解
- 用
@interface
定义注解 - 添加参数、默认值
把最常用的参数定义为value()
,推荐所有参数都尽量设置为默认值 - 用元注解配置注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
其中,必须设置@Target
和@Retention
,@Retention
一般设置为RUNTIME
,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited
和@Repeatable
。
处理注解
注解定义后其实也是一种class
,所有的注解都继承自java.lang.annotation.Annotation
,因此,读取注解,需要使用反射API。
注意,对于RUMTIME
类型的注解才有必要做这些处理.
通过程序处理注解可以实现相应的功能:
- 对JavaBean的属性值按规则进行检查
- JUnit会自动运行
@Test
标记的测试方法
使用注解
注解如何使用,完全由程序自己决定。例如JUnit是一个测试框架,它会自动运行所有标记为@Test
的方法。
定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。例如:
定义了一个@Range
注解,它定义了一个String
字段的规则:字段长度满足@Range
的参数定义。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
在某个JavaBean中,我们可以使用该注解
public class Person {
@Range(min=1, max=20)
public String name;
@Range(max=10)
public String city;
}
再编写一个Person
实例的检查方法,它可以检查Person
实例的String
字段长度是否满足@Range
的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String) {
String s = (String) value;
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
这样一来,我们通过@Range
注解,配合check()
方法,就可以完成Person
实例的检查。注意检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。
判断注解的存在性
判断某个注解是否存在于Class
、Field
、Method
或Constructor
:
Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
例如:
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
读取Annotation
使用反射API读取Annotation,若Annotation不存在则返回null
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
例如:
// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
由于方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解必须用一个二维数组来表示。
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
要读取方法参数的注解,我们先用反射获取Method
实例,然后读取方法参数的所有注解
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}
泛型
——即参数化类型,也就是说数据类型编程了一个可变的参数。
定义泛型的规则:
- 只能是类类型,不能是简单数据类型
- 泛型参数可以有多个
- 可以用使用
extends
语句或者super
语句,如<T extends superClass>
表示类型的上界,T
只能是superClass或其子类,<k super childClass>
表示类型的下界,K
只能是childClass或其父类 - 可以是通配符类型,比如常见的
Class<?>
泛型使用
向上转型
注意泛型的继承关系:可以把ArrayList<Integer>
向上转型为List<Integer>
(T
不能变!),但不能把ArrayList<Integer>
向上转型为ArrayList<Number>
(T
不能变成父类)
使用泛型
不指定泛型参数类型时,编译器会给出警告,且只能将<T>
视为Object
类型;
编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。例如,对于下面的代码:
List<Number> list = new ArrayList<Number>();
编译器看到泛型类型List<Number>
就可以自动推断出后面的ArrayList<T>
的泛型类型必须是ArrayList<Number>
,因此可以把代码简写为:
//可以省略后面的Number,编译器可以自动推断泛型类型
List<Number> list = new ArrayList<>();
泛型接口
还可以在接口中使用泛型,例如:
public interface Comparable<T> {
/**
* 返回-1: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回1: 当前实例比参数o大
*/
int compareTo(T o);
}
比如让一个Person
类实现Comparable<T>
接口:
class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public String toString() {
return this.name + "," + this.score;
}
}
编写泛型
编写一个泛型类
例如,写一个泛型类Pair<T>
(C++中的模版类)
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
静态方法中的泛型
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 对静态方法使用<T>:
public static <T> Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}
注意,上述代码中,静态方法的<T>
和Pair
类的<T>
已经不是同一个<T>
了,为了便于区分,我们把它改为另一种泛型类型,例如,<K>
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
这样才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。
多个泛型类型
例如,
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}
使用的时候,需要指出两种类型:
Pair<String, Integer> p = new Pair<>("test", 123);
擦拭法——Java泛型的实现方法
擦拭法:使用泛型的时候,我们编写的代码也是编译器看到的代码(即也是<T>
)。实际上,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T
视为Object
处理,但是,在需要转型的时候,编译器会根据T
的类型自动为我们实行安全的强制转型。
擦拭法决定了泛型<T>
:
- 不能是基本类型,例如:
int
,因为实际类型是Object
,Object
无法持有基本类型 - 不能获取带泛型类型的
Class
,例如:Pair<String>.class
和Pair<Integer>
获取到的是同一个Class
,因为它们编译后全部是Pair<Object>
- 不能判断带泛型类型的类型,例如:
x instanceof Paif<String>
- 不能实例化
T
类型,例如new T()
,实际上变成了new Object()
,类型不对。要实例化T
类型,必须借助额外的Class<T>
参数
public class Pair<T> {
private T first;
private T last;
//借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
使用的时候也必须传入Class<T>
//因为传入了Class<String>的实例,所以我们借助String.class就可以实例化String类型。
Pair<String> pair = new Pair<>(String.class);
泛型方法要防止重复定义方法,例如:equals(T obj)
,实际上会被擦拭成equals(Object obj)
,而这个方法是继承自Object
的,编译器会阻止一个实际上回变成重载的泛型方法定义,故而需要换个方法名以成功编译——,例如,same(T obj)
子类可以获取父类的泛型类型<T>
。
public class IntPair extends Pair<Integer> {
}
继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair
可以获取到父类的泛型类型Integer
。
通配符
无限定通配符
——<?>
,很少使用,可以用<T>
替换。它是所有<T>
类型的超类。
extends通配符
——上界通配符(Upper Bounds Wildcards)
使用类似<T extends Number>
定义泛型类时表示:
- 泛型类型限定为
Number
以及Number
的子类
例如,使用Pair<? extends Number>
使得方法接收所有泛型类型为Number
或Number
子类的Pair
类型。
使用类似<? extends Number>
通配符作为方法参数时表示:
- 方法内部可以调用获取
Number
引用的方法,例如:Number n = obj.getFirst()
; - 方法内部无法调用传入
Number
引用的方法(null
除外),例如:obj.setFirst(Number n)
换句话说,使用extends
通配符表示可以读,不能写。
super通配符
使用类似<T super Integer>
定义泛型类时表示:
- 泛型类型限定为
Integer
或Integer
的超类
使用类似<? super Integer>
通配符作为方法参数时表示:
- 方法内部可以调用传入
Integer
引用的方法,例如:obj.setFirst(Integer n)
- 方法内部无法调用获取
Integer
引用的方法(Object
除外),例如:Integer n = obj.getFirst()
换句话说,使用super
通配符表示只能写不能读
PECS原则
——Producer Extends, Consumer Super,即:如果需要返回T
,它是Producer,要使用extends
通配符;如果需要写入T
,它是Consumer,要使用super
通配符
泛型和反射
部分反射API是泛型,例如:Class<T>
,Constructor<T>
可以声明带泛型的数组,但不能用new
操作符创建带泛型的数组,而必须通过强制转型实现带泛型的数组:
Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!
Pair<String>[] ps = (Pair<String>[]) new Pair[2];
可以通过Array.newInstance(Class<T>, int)
创建T[]
数组,需要强制转型(否则擦拭后代码变为Object[]
)。
同时使用泛型和可变参数时要特别小心。
集合
Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
、Set
和Map
。
Java集合使用统一的Iterator
遍历,尽量不要使用遗留接口。
Java标准库自带的java.util
包提供了集合类:Collection
,它是所有其他集合类的根接口。在Collection
的基础上,Java的java.util
包主要提供了以下三种类型的集合:
List
:一种有序列表的集合,例如,按索引排列的Student
的List
Set
:一种保证没有重复元素的集合,例如,所有无重复名称的Student
的Set
Map
:一种通过键值(key-value)查找的映射集表集合,例如,根据Student
的name
查找对应Student
的Map
Java集合的设计有几个特点:
- 实现了接口和实现类相分离,例如,有序表的接口是
List
,具体的实现类有ArrayList
,LinkedList
等; - 支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
List<String> list = new ArrayList<>(); // 只能放入String类型
- Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的
另外,有一小部分集合类是遗留类,不应该继续使用:
Hashtable
:一种线程安全的Map
实现Vector
:一种线程安全的List
实现Stack
:基于Vector
实现的LIFO
的栈
还有一小部分接口是遗留接口,也不应该继续使用:Enumeration<E>
:已被Iterator<E>
取代
List
创建List
- 使用
ArrayList
和LinkedList
List<String> list = new ArrayList<>();
- 通过
List
接口提供的of()
方法,根据给定元素快速创建List
。(但是注意,List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常)
List<Integer> list = List.of(1, 2, 5);
遍历List
- 用
for
循环根据索引配合get(int)
方法遍历。但这种方式并不推荐,一是代码复杂,二是因为get(int)
方法只有ArrayList
的实现是高效的,换成LinkedList
后,索引越大,访问速度越慢。
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
- 用迭代器
Iterator
来访问List
。Iterator
本身也是一个对象,但它是由List
的实例调用iterator()
方法的时候创建的。Iterator
对象知道如何遍历一个List
,并且不同的List
类型,返回的Iterator
对象实现也是不同的。虽然它貌似比直接使用索引更复杂,但它总是具有最高的访问效率。
Iterator
对象有两个方法:boolean hasNext()
判断是否有下一个元素,E next()
返回下一个元素。
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
for each
循环(本身就是帮我们使用Iterator
遍历)。实际上,只要实现了Iterator
接口的集合类都可以直接用for each
循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each
循环变成Iterator
的调用。
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
常用方法
考察List<E>
接口,几个主要的接口方法:
void add(E e)
,在末尾添加一个元素void add(int index, E e)
,在指定索引添加一个元素int remove(int index)
,删除指定索引的元素int remove(Object e)
,删除某个元素E get(int index)
,获取指定索引的元素int size()
,获取链表大小(包含元素的个数)boolean contains(Object o)
,判断是否包含某个指定元素int indexOf(Object o)
,返回某个元素的索引,如果元素不存在则返回-1
ArrayList
和LinkedList
都实现了List的接口。两者的比较如下:
List
接口允许我们添加重复的元素,还允许添加null
List和Array转换
把List
变为Array
有三种方法:
- 调用
toArray()
方法直接返回一个Object[]
数组,但这种方法会丢失信息,所以实际应用很少
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
for (Object s : array) {
System.out.println(s);
}
}
}
- 给
toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
中
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}
事实上,这个toArray(T[])
方法的泛型参数<T>
并不是List
接口定义的泛型参数<E>
,所以,我们实际上可以传入其他类型的数组,例如:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Number[] array = list.toArray(new Number[3]);
for (Number n : array) {
System.out.println(n);
}
}
}
但是,如果我们传入类型不匹配的数组,例如,String[]
类型的数组,由于List
的元素是Integer
,所以无法放入String
数组,这个方法会抛出ArrayStoreException
。
此外,根据List
接口的文档,我们可以知道:
如果传入的数组不够大,那么List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List
元素还要多,那么填充完元素后,剩下的数组元素一律填充null
- 通过
List
接口定义的T[] toArray(IntFunction<T[]>generator)
方法:
Integer[] array = list.toArray(Integer[]::new);
把Array
变为List
:
- 通过
List.of(T...)
方法
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
- 对于JDK 11之前的版本,可以使用
Arrays.asList(T...)
方法
要注意的是:返回的List
不一定就是ArrayList
或者LinkedList
,因为List
只是一个接口,如果我们调用List.of()
,它返回的是一个只读List
:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
list.add(999); // UnsupportedOperationException
}
}
重载equals()方法
在List
中查找元素时,List
的实现类通过元素的equals()
方法比较两个元素是否相等。
因此,放入的元素必须正确重载equals()
方法(当然String
、Integer
这些Java标准库已有定义)。
理论上,equals()
方法要求我们必须满足以下条件:
- 自反性(Reflexive):对于非
null
的x
来说,x.equals(x)
必须返回true
- 对称性(Symmetric):对于非
null
的x
和y
来说,如果x.equals(y)
为true
,则y.equals(x)
也必须为true
- 传递性(Transitive):对于非
null
的x
、y
、z
来说,如果x.equals(y)
为true
,y.equals(z)
也为true
,那么x.equals(z)
也必须为true
- 一致性(Consistent):对于非
null
的x
和y
来说,只要x
和y
状态不变,则x.equals(y)
总是一致地返回true
或false
- 对于
null
的比较:即x.equals(null)
永远返回false
实践上(说人话),equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑——即哪些字段相等,就认为实例相等
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
- 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较
如果不在List
中查找元素,就不必重载equals()
方法:比如,如果不调用List
的contains()
、indexOf()
这些方法,那么放入的元素就不需要实现equals()
方法
Map
————能高效通过key
快速查找value
(元素)
当我们调用put(K key, V value)
方法时,就把key
和value
做了映射并放入Map
。【注意,如果放入的key
已经存在,put()
方法会返回被旧的value
,并把它替换为新的,否则返回null
】
当我们调用V get(K key)
时,就可以通过key
获取到对应的value
,如果key
不存在,则返回null
可以调用boolean containsKey(K key)
,查询某个key
是否存在
最常用的一种Map
实现是HashMap
可以通过for each
遍历keySet()
,也可以通过for each
遍历entrySet()
,直接获取key-value
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
}
}
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
注意,遍历Map时,不可假设输出的key是有序的!
使用HashMap
——以空间换时间,内部的Key是无序的
重载equals()方法
在Map
内部,对key
做比较是通过equals()
实现的,所以正确使用Map
必须保证:作为key
的对象必须正确重载equals()
方法——相等的两个key
实例调用equals()
必须返回true
;
重载hashCode()方法
要正确使用HashMap
,作为key
的类必须正确重载equals()
和hashcode()
方法。
通过key
计算索引的方式就是调用key
对象的hashCode()
方法,它返回一个int
整数。HashMap
正是通过这个方法直接定位key
对应的value
的索引,继而直接返回value
hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等 - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等
实现hashCode()
方法可以通过Objects.hashCode()
辅助方法实现
使用TreeMap
接口SortedMap
,在内部会对Key进行排序,它的实现类是TreeMap
使用TreeMap
时,放入的Key必须实现Comparable
接口。String
、Integer
这些类已经实现了Comparable
接口,因此可以直接作为Key使用。
如果作为Key的class没有实现Comparable
接口,那么必须在创建TreeMap
时同时指定一个自定义排序算法
TreeMap
不用重载equals()
方法和hashCode()
,它依赖Key的compareTo()
方法或者Comparator.compare()
方法。【所以要严格按照compare()
规范实现比较逻辑(a<b
,返回负数,通常是-1
;如果a==b
,则返回0
;如果a>b
,则返回正数,通常是1
),否则TreeMap
不能正常工作】
Lambda表达式
Lambda表达式是Java SE8中一个重要的新特性,允许我们通过表达式来代替功能接口。
——接受函数当作输入(引数)和输出(传出值)
语法
——parameter -> expression body
parameter
,一个括号内用逗号分隔的参数列表,参数即函数式接口里面方法的参数
expression body
,方法体,可以是表达式和代码块
Lambda表达式的几个最重要的特征:
- 可选的类型声明:可以不用声明参数的类型,编译器可以从参数的值来推断它是什么类型
- 可选的参数周围的括号:单个参数情况可以不加括号;但多个参数的情况必须要加括号
- 可选的大括号:如果表达式体里面只有一个语句,可以不必用大括号括起来
- 可选的返回关键字:如果表达式体只有单个表达式用于值的返回,那么编译器会自动完成这一步。若要指示表达式来返回某个值,则需要使用大括号
//LambdaTest.java
public class LambdaTest {
public static void main(String[] args) {
LambdaTest tester = new LambdaTest();
MathOperation addition = (int a, int b) -> a+b;
MathOperation subtraction = (a,b) -> a-b;
MathOperation multiplication = (int a,int b) -> {return a*b;};//里面的分号别漏了
MathOperation division = (int a,int b) -> a/b;
System.out.println("10+5="+tester.operate(10,5,addition));
System.out.println("10-5="+tester.operate(10,5,subtraction));
System.out.println("10*5="+tester.operate(10,5,multiplication));
System.out.println("10/5="+tester.operate(10,5,division));
GreetingService greetService1 = message -> System.out.println("Hello "+message);
GreetingService greetService2 = (message) -> System.out.println("Hello "+ message);
greetService1.sayMessage("Shiyanlou");
greetService2.sayMessage("Classmate");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation) {
return mathOperation.operation(a,b);
}
}
运行结果:
方法引用
——用于直接访问类或者实例的已经存在的方法或者构造方法。
语法:
- 构造器引用:
Class::new
,或者更一般的Class<T>::new
,要求构造器方法没有参数 - 静态方法引用:
Class::static_method
- 特定类的任意对象方法引用:
Class::method
- 特定对象的方法引用:
instance::method
例如:
List<String> names = new ArrayList<>();
names.add("Peter");
names.add("Linda");
names.add("Smith");
names.add("Zack");
names.add("Bob");
names.forEach(System.out::println);
函数式接口
——只包含一个方法的接口。例如,带有单个compareTo
方法的比较接口。Java 8开始定义了大量的函数式接口来广泛地用于lambda表达式。其中java.util.function
包中包含了大量的函数式接口,基本可以满足我们日常的开发需求。
下面是部分函数式接口的列表:
更多的接口可以参考 Java 官方 API 手册:java.lang.Annotation Type FunctionalInterface
。在实际使用过程中,加有@FunctionalInterface
注解的方法均是此类接口,位于java.util.Funtion
包中。
Stream流
——Java 8开始的一个新的抽象层。通过使用Stream,开发者可以通行声明式数据处理,以及简单地利用多核处理体系而不用写特定的代码
事实上,Stream代表了来自某个源的对象的序列,这些序列支持聚集操作。下面是Stream的一些特性:
- 元素序列:Stream以序列的形式提供了特定类型的元素的集合。根据需求,它可以获得和计算元素,但不会存储任何元素
- 源:Stream可以将集合、数组和I/O资源作为输入源
- 聚集操作:Stream支持诸如
fileter
、map
、limit
、reduce
等的聚集操作 - 流水技术:许多Stream操作返回了流本身,故它们的返回值可以以流水的形式存在。这些操作称之为中间操作,并且它们的功能就是负责输入、处理和向目标输出。
collect()
方法是一个终结操作,通常存在于流水线操作的末端,来标记流的结束 - 自动迭代:Stream的操作可以基于已提供的源元素进行内部的迭代,而集合则需要显式的迭代
相关的方法介绍
产生流
集合的接口有两个方法来产生流:
stream()
:该方法返回一个将集合视为源的连续流parallelStream()
:该方法返回一个将集合视为源的并行流
其他相关方法
forEach
:该方法用于对Stream中的每个元素进行迭代操作。例如:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
map
:该方法用于将每个元素映射到对应的结果上。例如,
System.out.println("数组去重乘2求和:" + Arrays.stream(arr).distinct().map((i) -> i * 2).count());
filter
:该方法用于过滤满足条件的元素。例如:
List<String>strings = Arrays.asList("efg", "", "abc", "bc", "ghij","", "lmn");
//get count of empty string
long count = strings.stream().filter(string -> string.isEmpty()).count();
limit
:该方法用于减少 Stream 的大小
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
sorte
:该方法用于对Stream排序
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
并行处理
ParallelStream 是 Stream 用于并行处理的一种替代方案。例如:
List<String> strings = Arrays.asList("efg", "", "abc", "bc", "ghij","", "lmn");
// 获得空字符串的计数
long count = strings.parallelStream().filter(String::isEmpty).count();
当然,在连续的 Stream 与并行的 Stream 之间切换是很容易的。
Collector
Collector用于合并Stream的元素处理结果。它可以用于返回一个字符串列表。
List<String>strings = Arrays.asList("efg", "", "abc", "bc", "ghij","", "lmn");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("Filtered List: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("Merged String: " + mergedString);
Stream处理完成后使用Collector来统计数据,例如:
List<Integer> numbers = Arrays.asList(2, 3, 3, 2, 5, 2, 7);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest number in List : " + stats.getMax());
System.out.println("Lowest number in List : " + stats.getMin());
System.out.println("Sum of all numbers : " + stats.getSum());
System.out.println("Average of all numbers : " + stats.getAverage());
FlatMap
——用于将多个流合并为一个流。使用FlatMap时,表达式的返回值必须是Stream类型。而Map用于将一种流转化为另外一种流。
使用示例:
import java.util.stream.Stream;
public class MergerStream {
public static void main(String[] args) {
Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);
Stream<Integer> stream3 = Stream.of(7, 8, 9);
Stream<Integer> mergerStream = Stream.of(stream1, stream2, stream3).flatMap((i) -> i);
mergerStream.forEach(System.out::print);
}
}
参考教程
[1] Java教程 - 廖雪峰的官方网站(https://www.liaoxuefeng.com/wiki/1252599548343744)
[2] Java语言编程基础 - 实验楼(https://www.shiyanlou.com/courses/1230/learning/)