JDK《Java教程-廖雪峰》笔记

  1. 1.基础
    1. 1.名词解释
    2. 2.数据类型
    3. 3.数组操作
    4. 4.面向对象
      1. 1.classpath
      2. 2.模块
    5. 3.访问权限控制(略)
    6. 4.内部类(略)
    7. 5.类加载(略)
    8. 提问
  2. 2.Java核心类
    1. Object类
    2. String 类(略)
      1. 去除首尾空白字符
      2. 替换子串
      3. 分割字符串
      4. 拼接字符串
      5. 格式化字符串
    3. 提问
  3. 3.异常处理
    1. 提问
  4. 4.反射(全)
    1. 1.class类
      1. 1.什么是反射?
      2. 2.获取class实例
      3. 3.可以从Class对象获取哪些信息?
      4. 4.数组和基本类型的Class对象
      5. 5.可以用Class对象创建新对象
      6. 6.动态加载
      7. 小结
    2. 2.访问字段
      1. 1.获取Feild对象
      2. 2.获取Feild值
      3. 3.设置Feild值
      4. 小结
    3. 3.调用方法
      1. 1.获取Method对象
      2. 2.调用方法
      3. 3.调用静态方法
      4. 4.调用非public方法
      5. 5.多态
    4. 4.调用构造方法
    5. 5.获取继承关系,即子父类的class等
      1. 1.获取class对象
      2. 2.获取父类class:getSuperclass()
      3. 3.获取interface:getInterfaces()
      4. 4.获取继承关系:isAssignableFrom()
    6. 6.动态代理
    7. 提问:
  5. 5.注解(全)
    1. 1.元注解
      1. 1.@Target:生效位置
      2. 2.@Retention:生命周期
      3. 3.@Repeatable:注解是否可重复
      4. 4.@Inherited:是否可继承
    2. 2.处理注解
      1. 1.读取注解信息
      2. 2.使用注解
    3. 提问
  6. 6.泛型
    1. 1.使用泛型
    2. 2.编写泛型
    3. 3.擦除法特性
      1. 1.Java泛型局限
      2. 2.有些方法不能覆写
      3. 3.泛型继承
      4. 4.Java的类型系统
    4. 4.extends通配符
    5. 5.super通配符
    6. 6.泛型和反射(待补充)
    7. 提问
  7. 7.集合(简略)
    1. ArrayList
    2. LinkedList
    3. HashMap
  8. 8.IO(简略)
    1. 五大 I/O 模型比较
  9. 9.日期和时间(略)
  10. 10.正则(简介)
  11. 11.加密(暂略)
  12. 12.函数式编程
    1. 1.FunctionalInterface
    2. 2.方法引用
    3. 3.使用Stream
      1. 1.创建Stream
      2. 2.使用map
      3. 3.使用filter
      4. 4.使用reduce
      5. 5.输出集合
      6. 6.其他操作
        1. 排序sorted()
        2. 去重distinct()
        3. 截取limit()
        4. 合并concat()
        5. flatMap
        6. 并行parallel()
        7. 其他聚合方法count()、max()、allMatch()
  13. 13.其他(略)

主要参考:Java编程思想、廖雪峰官网、菜鸟教程等

1.基础

1.名词解释

Java之父:James Gosling

  • JDK:Java Development Kit

  • JRE:Java Runtime Environment

  • JSR规范:Java Specification Request

  • JCP组织:Java Community Process

为了保证Java语言的规范性,SUN供了搞了一个JSR规范,凡是想给Java平台加一个功能,比如说访问数据库的功能,大家要先创建一个JSR规范,定义好接口,这样,各个数据库厂商都按照规范写出Java驱动程序,开发者就不用担心自己写的数据库代码在MySQL上能跑,却不能跑在PostgreSQL上。

所以JSR是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP。

一个JSR规范发布时,为了让大家有个参考,还要同时发布一个“参考实现”,以及一个“兼容性测试套件”:

  • RI:Reference Implementation
  • TCK:Technology Compatibility Kit

比如有人提议要搞一个基于Java开发的消息服务器,这个提议很好啊,但是光有提议还不行,得贴出真正能跑的代码,这就是RI。如果有其他人也想开发这样一个消息服务器,如何保证这些消息服务器对开发者来说接口、功能都是相同的?所以还得提供TCK。

通常来说,RI只是一个“能跑”的正确的代码,它不追求速度,所以,如果真正要选择一个Java的消息服务器,一般是没人用RI的,大家都会选择一个有竞争力的商用或开源产品。

2.数据类型

8个基本数据类型:

  • 整数类型:byte,short,int,long
  • 浮点数类型:float,double
  • 字符类型:char
  • 布尔类型:boolean。1个字节。

所占字节数如下图:

image-20211015083925695

image-20211015100634014

2^7 = 128

2^16 = 65536

提问:

Java 八大基本数据类型是什么?各占多少位?

自动拆包装包什么意思?JVM底层原理是什么?

Int和byte等是怎么自动转型的?

3.数组操作

多维数组,以二维数组举例。

public class Main {
    public static void main(String[] args) {
        int[][] ns = {
            { 1, 2, 3, 4 },
            { 5, 6, 7, 8 },
            { 9, 10, 11, 12 }
        };
        System.out.println(ns.length); // 3
    }
}

ns包含了三个元素,每个元素又是一个数组,在内存中结构如下图,很直观:

image-20211015084623630

ns[x][y],x就是行,y就是列。要先找到行(第一层元素),才能找到列(第二层元素)。

所以ns[1][2]= 7。 ns.length = 3,ns[0].length = 4。

4.面向对象

1.classpath

classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class

在启动JVM时设置classpath是推荐的做法。 实际上就是给java命令传入-classpath-cp参数。如java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello

没有设置系统环境变量,也没有传入-cp参数,那么JVM默认的classpath.,即当前目录。

2.模块

如果是自己开发的程序,除了一个自己的app.jar以外,还需要一堆第三方的jar包,运行一个Java程序,一般来说,命令行写这样:

java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main

jar只是用于存放class的容器,它并不关心class之间的依赖。

从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块。

为了表明Java模块化的决心,从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar分拆成了几十个模块,这些模块以.jmod扩展名标识,可以在$JAVA_HOME/jmods目录下找到它们:

  • java.base.jmod
  • java.compiler.jmod
  • java.datatransfer.jmod
  • java.desktop.jmod

这些.jmod文件每一个都是一个模块,模块名就是文件名。例如:模块java.base对应的文件就是java.base.jmod。模块之间的依赖关系已经被写入到模块内的module-info.class文件了。所有的模块都直接或间接地依赖java.base模块,只有java.base模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从Object直接或间接继承而来。

把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。

提问:

JDK1.9的模块是什么?用来解决什么问题的?怎么用?

3.访问权限控制(略)

把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。

一个.java文件只能包含一个public类,但可以包含多个非public类。

final修饰符不是访问权限,它可以修饰classfieldmethod:类不能被继承,方法不能被覆写,属性不能被二次赋值。

4.内部类(略)

5.类加载(略)

提问

类加载过程说一下?类初始化过程说一下?初始化顺序?

2.Java核心类

StringBuild:支持缓存。(线程安全?)

其他:String类等。(略)

Object类

提问:

Object类常用的方法有哪些?

getClass(),equals()、toString()、hashCode()、clone(),wait()、notify()、notifyAll(),finalize()。

String 类(略)

Java字符串的一个重要特点就是字符串不可变。实现原理:内部的private final char[]字段,以及没有任何修改char[]的方法。

比较相等时,注意缓存的情况:

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}
// ture。true。Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池。
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "HELLO".toLowerCase();
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}
// false。true。

StringBuffer,是Java早期的一个StringBuilder的线程安全版本,它通过同步来保证多个线程操作StringBuffer也是安全的,但是同步会带来执行速度的下降。

常用方法:

去除首尾空白字符

使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t\r\n

"  \tHello\r\n ".trim(); // "Hello"

注意:trim()并没有改变字符串的内容,而是返回了一个新字符串。

另一个strip()方法也可以移除字符串首尾空白字符。它和trim()不同的是,类似中文的空格字符\u3000也会被移除:

"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"

String还提供了isEmpty()isBlank()来判断字符串是否为空和空白字符串:

"".isEmpty(); // true,因为字符串长度为0
"  ".isEmpty(); // false,因为字符串长度不为0
"  \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串

要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:

String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"

另一种是通过正则表达式替换:

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"
格式化字符串

字符串提供了formatted()方法和format()静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:

// String Run

有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:

  • %s:显示字符串;
  • %d:显示整数;
  • %x:显示十六进制整数;
  • %f:显示浮点数。

占位符还可以带格式,例如%.2f表示显示两位小数。如果你不确定用啥占位符,那就始终用%s,因为%s可以显示任何数据类型。要查看完整的格式化语法,请参考JDK文档

提问

String是线程安全的吗?

StringBuffer与StringBuilder什么区别?

String整体类的结构、常用方法、底层怎么实现?

3.异常处理

异常的继承结构

1598511670173

Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorException

  • Error表示严重的错误,程序对此一般无能为力。如:StackOverflowError栈溢出、OutOfMemoryError内存耗尽、NoClassDefFoundError无法加载类等。

  • Exception则是用户还可以抢救一下。

Exception又分为两大类:

  • RuntimeException以及它的子类。
  • RuntimeException(包括IOExceptionReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,包括Exception及其除RuntimeException以外的子类,这些也叫Checked Exception。
  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。图中蓝色部分。

补充:

CheckedException是应用程序逻辑处理的一部分,预先就可以预期到的,应该检查捕获并处理。例如:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

RuntimeException一般是程序逻辑编写不对造成的,应该修复程序本身。例如:

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

提问

简述异常的继承结构?什么是Error?什么是受检异常?什么是运行时异常?把常见的error、受检异常、运行时异常都举例说明一下。

4.反射(全)

反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。

反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

大概的用法就是,想办法拿到Class对象,然后就可以新建对象、调用字段、调用方法、调用构造方法、获取继承关系等,为所欲为。另外动态代理也需要传入接口的Class对象。

1.class类

1.什么是反射?

除了int等基本类型外,Java的其他类型全部都是class(类似于C语言的struct?)。class(包括interface)的本质就是数据类型(Type)。因为要有继承关系的数据类型才能赋值。

JVM在第一次读取到一种class类型时,将其加载进内存,并为其创建一个Class类型的实例,再关联起来。注意:这里的Class类型是一个名叫Classclass。它长这样:

public final class Class {
    private Class() {}
}

String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:

Class cls = new Class(String);

这个Class实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例的。

所以,JVM持有的每个Class实例都指向一个数据类型(classinterface),一个Class实例包含了该class的所有完整信息。

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。

这种通过Class实例获取class信息的方法称为反射(Reflection)。

2.获取class实例

总共有三种方法

方法一:通过类,直接通过一个class的静态变量class获取:

Class cls = String.class;

方法二:通过对象,可以通过对象的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三:通过完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

因为Class实例在JVM中是唯一的,所以上述方法获取的Class实例是同一个实例。可以用==比较,是相等的。

3.可以从Class对象获取哪些信息?

参考如下:

static void printClassInfo(Class cls) {
        System.out.println("Class name: " + cls.getName());
        System.out.println("Simple name: " + cls.getSimpleName());
        if (cls.getPackage() != null) {
            System.out.println("Package name: " + cls.getPackage().getName());
        }
        System.out.println("is interface: " + cls.isInterface());
        System.out.println("is enum: " + cls.isEnum());
        System.out.println("is array: " + cls.isArray());
        System.out.println("is primitive: " + cls.isPrimitive());
    }
4.数组和基本类型的Class对象

注意到数组(例如String[])也是一种Class,而且不同于String.class,它的类名是[Ljava.lang.String

此外,JVM为每一种基本类型如int也创建了Class,通过int.class访问。

5.可以用Class对象创建新对象

如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:

// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();

上述代码相当于new String()。通过*class.newInstance()可以创建类实例*,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过class.newInstance()被调用。

6.动态加载

JVM在第一次需要用到class时才加载,这就是JVM动态加载class的特性。

动态加载class的特性对于Java程序非常重要。利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:

// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
    factory = createLog4j();
} else {
    factory = createJdkLog();
}

boolean isClassPresent(String name) {
    try {
        Class.forName(name);
        return true;
    } catch (Exception e) {
        return false;
    }
}

这就是为什么我们只需要把Log4j的jar包放到classpath中,Commons Logging就会自动使用Log4j的原因。

小结

JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;

获取一个class对应的Class实例后,就可以获取该class的所有信息;

通过Class实例获取class信息的方法称为反射(Reflection);

JVM总是动态加载class,可以在运行期根据条件来控制加载class。

2.访问字段

1.获取Feild对象

class对象提供了以下几个方法来获取字段

  • Field getField(name):根据字段名获取某个publicfield(包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

一个Field对象包含了一个字段的所有信息,用以下方法获取:

  • getName():返回字段名称,例如,"name"
  • getType():返回字段类型,也是一个Class实例,例如,String.class
  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。可以用Modifier.isFinal(m)、isStatic(m)、isPublic(m)、isProtected(m)、isPrivate(m)等来判断属性。

private的也能拿到。

2.获取Feild值

feild::get(Object obj)方法可以获取到字段的值。

遇到private属性时,可以用field::setAccessible(true)来解锁

不过setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。

使用示例:

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);
        Object value = f.get(p);
        System.out.println(value); // "Xiao Ming"
        
        f.set(p, "Xiao Hong");
        System.out.println(p.getName()); // "Xiao Hong"
    }
}

class Person {
    private String name;
    public Person(String name) { this.name = name;}
}
3.设置Feild值

通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。示例见前面的代码f.set(p, "Xiao Hong")

小结

Java的反射API提供的Field类封装了字段的所有信息:

通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()

通过Field实例可以获取字段信息:getName()getType()getModifiers()

通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。

通过反射读写字段是一种非常规方法,它会破坏对象的封装。

3.调用方法

1.获取Method对象

class对象,用它的以下方法获取:

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类),Class...为方法参数。
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类),Class...为方法参数。
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类

private的method也能拿到

示例如下:

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";
    }
}
2.调用方法

用Method对象调用即可。即method.invoke(Object obj, Object... parems)

示例:

String s = "Hello world";
String r = s.substring(6); // "world"

// 如果用反射来调用substring方法
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);
    }
}
3.调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)为例:

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);
    }
}
4.调用非public方法

和Field类似,设置method.setAccessible(true)即可。

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name);
    }
}

class Person {
    String name;
    private void setName(String name) {
        this.name = name;
    }
}
5.多态

多态时调用的是子类还是父类呢?使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)

如下代码:

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取Person的hello方法:
        Method h = Person.class.getMethod("hello");
        // 对Student实例调用hello方法:
        h.invoke(new Student());

        // 以上反射代码也遵循多态原则,相当于如下:
        Person p = new Student();
        p.hello();
    }
}

class Person {
    public void hello() {
        System.out.println("Person:hello");
    }
}

class Student extends Person {
    public void hello() {
        System.out.println("Student:hello");
    }
}

4.调用构造方法

我们通常使用new操作符创建新的实例:

Person p = new Person();

如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();

不过以上只能提供调用public无参构造方法。

要获取所有构造方法,反射提供了constructor对象

constructor对象和method对象类似,通过class对象获取方式如下

  • getConstructor(Class...):获取某个publicConstructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getDeclaredConstructors():获取所有Constructor

注意*Constructor总是当前类定义的构造方法,和父类无关*,因此不存在多态的问题。

同样的,调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

5.获取继承关系,即子父类的class等

1.获取class对象

三种方式,前面已经提过了:

  • Class获取:Class cls = String.class; // 获取到String的Class
  • 用*object.getClass()*获取:Class cls = str.getClass(); // s是String,因此获取到String的Class
  • 用*Class.forName(“”)*获取:Class s = Class.forName("java.lang.String");

三种方式获取到的class对象都是同一个,JVM里它是唯一的。

2.获取父类class:getSuperclass()

class.getSuperclass()获取即可。除了Object类,其他所有类都有父类。

public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();			// number
        System.out.println(n);
        Class o = n.getSuperclass();			// object
        System.out.println(o);
        System.out.println(o.getSuperclass());	// null
    }
}
3.获取interface:getInterfaces()

class.getInterfaces()获取即可

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class;
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}
// java.lang.Comparable
// java.lang.constant.Constable
// java.lang.constant.ConstantDesc

getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口

此外,对所有interfaceClass调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()

System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()

如果一个类没有实现任何interface,那么getInterfaces()返回空数组。

4.获取继承关系:isAssignableFrom()

当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()

// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

6.动态代理

JDK提供了一个Proxy.newProxyInstance()在运行期才创建了一个接口的对象

一个最简单的动态代理实现如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

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

在运行期动态创建一个interface实例的方法如下

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过*Proxy.newProxyInstance()创建interface实例*,它需要3个参数:
    1. 类加载器:使用的ClassLoader,通常就是接口类的ClassLoader
    2. 接口:需要实现的接口数组,至少需要传入一个接口进去;
    3. InvocationHandler:用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,把上面的动态代理改写为静态实现类大概长这样:

public class HelloDynamicProxy implements Hello {
    InvocationHandler handler;
    public HelloDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    public void morning(String name) {
        handler.invoke(
           this,
           Hello.class.getMethod("morning", String.class),
           new Object[] { name });
    }
}

其实就是JVM帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码)。

提问:

反射是什么?是用来解决什么问题的?简要说说反射的原理(无非就是玩class)?

反射就是在运行期获取对象信息的行为。用来解决对某个对象一无所知时,怎么使用它的问题。原理大概就是,JVM在加载class文件时,会对应的生成唯一的Class对象,它包含了这个class的所有信息。所以我们拿到Class对象,就获取到了某未知对象的所有信息。

反射和动态代理什么关系?动态代理是jdk1.3加到反射包里的,用来在运行时动态的生成接口的对象。

反射怎么获取feild对象?怎么获取值?

反射怎么获取method对象?怎么使用?

反射怎么获取constructor对象?怎么使用?

反射怎么获取父类class对象?如何判断两个class对象的继承关系?

动态代理怎么实现?和反射有什么联系?

5.注解(全)

注解可以被编译器打包进入class文件,是一种用作标注的“元数据”。

1.元注解

有以下4个元注解

  • @Target:生效位置:类/接口、字段、方法、构造方法、方法参数。
  • @Retention:生命周期:仅编译期、class文件、运行期
  • @Repeatable:是否可重复。
  • @Inherited:子类是否可继承父类的注解。仅针对@Target(ElementType.TYPE)类型的注解有效,且仅针对class的继承,对interface的继承无效。

下面进行详细介绍:

1.@Target:生效位置

定义Annotation能够被应用于源码的哪些位置:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER
// 定义注解@Report可用在方法或字段上,可以把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:
@Target({
    ElementType.METHOD,
    ElementType.FIELD
})
public @interface Report {
    ...
}
2.@Retention:生命周期

定义了Annotation的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅class文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

如果@Retention不存在,则该Annotation默认为CLASS。因为通常我们自定义的Annotation都是RUNTIME

@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}
3.@Repeatable:注解是否可重复

可以定义Annotation是否可重复。这个注解应用不是特别广泛。

@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

@Target(ElementType.TYPE)
public @interface Reports {
    Report[] value();
}

// 经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
4.@Inherited:是否可继承

@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效:

@Inherited
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

// 在使用的时候,如果一个类用到了@Report:
@Report(type=1)
public class Person {
}
// 则它的子类默认也定义了该注解:
public class Student extends Person {
}

自定义注解定义示例:

必须设置@Target@Retention@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited@Repeatable

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

2.处理注解

1.读取注解信息

这里只讨论如何读取RUNTIME类型的注解。

(1)读取class的注解:

Java提供的使用反射API读取Annotation的方法包括:

*判断某个注解是否存在于ClassFieldMethodConstructor*:

  • class.isAnnotationPresent(Class class)
  • field.isAnnotationPresent(Class class)
  • method.isAnnotationPresent(Class class)
  • constructor.isAnnotationPresent(Class class)
// 例:判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);

使用反射API读取annotation对象

  • class.getAnnotation(Class annotationClass)
  • field.getAnnotation(Class annotationClass)
  • method.getAnnotation(Class annotationClass)
  • constructor.getAnnotation(Class annotationClass)

例如:

// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();

// 方法一:先判断Annotation是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}

// 方法二:直接读取Annotation,如果Annotation不存在,将返回null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}

(2)读取方法、字段和构造方法的Annotation和Class类似。

(3)读取方法参数的Annotation稍微麻烦点:因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解必须用一个二维数组来表示

// 注解使用:
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;
    }
}
2.使用注解

使用示例:

(1)定义注解

// 用于字段,规定值要满足长度
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}

(2)使用注解

public class Person {
    @Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}

(3)编写处理程序

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

提问

元注解有哪些?

如何使用自定义注解?举例说明

6.泛型

1.使用泛型

示例,person列表按name排序:

// sort
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);	// Person不实现Comparable<Person>,我们会得到ClassCastException
        System.out.println(Arrays.toString(ps));
    }
}

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

2.编写泛型

Class级别的泛型,声明在类名后面即可:

public class Pair<T, K> {
    private T first;
    private K second;
    // ...
}

Static级别的方法不能使用类级别的泛型,可使用方法级别的泛型。

方法级别的泛型,声明在修饰符后、返回值前即可:

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

3.擦除法特性

Java语言的泛型实现方式是擦拭法(Type Erasure)。

擦拭法是指:虚拟机对泛型其实一无所知,所有的工作都是编译器做的

Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型

1.Java泛型局限

因此,Java泛型有以下局限:

  • 局限一:<T>不能是基本类型,例如int,因为实际类型是ObjectObject类型无法持有基本类型。

  • 局限二:无法取得带泛型的class。不同类型泛型获取class时,获取到的是同一个class,编译后它们全部都是Pair<Object>。观察以下代码

    public class Main {
        public static void main(String[] args) {
            Pair<String> p1 = new Pair<>("Hello", "world");
            Pair<Integer> p2 = new Pair<>(123, 456);
            Class c1 = p1.getClass();
            Class c2 = p2.getClass();
            System.out.println(c1==c2); // true
            System.out.println(c1==Pair.class); // true
        }
    }
    
    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;
        }
    }
    
  • 局限三:无法判断带泛型的类型

    // 并不存在Pair<String>.class,而是只有唯一的Pair.class。
    Pair<Integer> p = new Pair<>(123, 456);
    // Compile error:
    if (p instanceof Pair<String>) {
    }
    
  • 局限四:不能实例化T类型

    public class Pair<T> {
        private T first;
        private T last;
        public Pair() {
            // Compile error:
            first = new T();	// 相当于new Object();
            last = new T();		// 相当于new Object();
        }
    }
    // 要实例化T类型,我们必须借助额外的Class<T>参数:
    Pair<String> pair = new Pair<>(String.class);
    
2.有些方法不能覆写

以下方法无法通过编译:

// 因为,定义的`equals(T t)`方法实际上会被擦拭成`equals(Object t)`,而这个方法是继承自Object的,equals(T t)泛型方法的声明会误认为是方法覆写,编译器不会让通过
public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}
// 换个方法名就OK
public class Pair<T> {
    public boolean same(T t) {
        return this == t;
    }
}
3.泛型继承

一个类可以继承自一个泛型类

public class IntPair extends Pair<Integer> {
}
// 子类IntPair并没有泛型类型,所以,正常使用即可,不过只能用Integer类型:
IntPair ip = new IntPair(1, 2);

在父类是泛型类型的情况下,编译器会把类型T保存到子类的class文件中

在继承泛型类型时,子类可以获取父类的泛型类型

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
    public static void main(String[] args) {
        Class<IntPair> clazz = IntPair.class;
        Type t = clazz.getGenericSuperclass();
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
            Type firstType = types[0]; // 取第一个泛型类型
            Class<?> typeClass = (Class<?>) firstType;
            System.out.println(typeClass); // Integer
        }
    }
}

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

class IntPair extends Pair<Integer> {
    public IntPair(Integer first, Integer last) {
        super(first, last);
    }
}
4.Java的类型系统

Java引入了泛型,所以,只用Class来标识类型已经不够了。实际上,Java的类型系统结构如下:

image-20210301173051220

4.extends通配符

声明示例:

public class Pair<? extends Number> {...}

<? extends Number>通配符的一个重要限制:方法参数签名,例如,setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)。因为擦除后都是Object。

此外,**Pair\<Integer\> 不是Pair\<Number\>的子类**。

5.super通配符

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能写不能读。与extends相反。只要抓住擦除法,就很好理解。

无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

6.泛型和反射(待补充)

如何拿到泛型的类型?

提问

泛型是什么?用来解决什么问题?Java是用什么方式来实现泛型的?JVM底层是怎么处理的?使用泛型要注意哪些坑?

Java的Type有哪些类型

7.集合(简略)

ArrayList

源码:

  • 数据结构:使用一个数组名为elementData来存数据。
  • 构造时不传初始数量则数组取{}。传了数量则默认改数量长度。
  • 扩容时,每次扩容1.5倍:capcity + (capacity >> 1)。小于10则一次性扩容到10,否则扩容1.5倍。扩容时考虑Integer.max限制。扩容时机是,发现不够用的时候才会扩容。
  • 扩容调用Arrays.copyOf(),进一步调用System.arraycopy,底层是调用native方法,效率较高。
  • 扩容是老数组复制到新数组,操作代价很高。所以尽量赋初值,避免扩容。
  • 也可根据实际需求,通过调用ensureCapacity()方法来手动增加ArrayList实例的容量。
  • 缩容:需要注意的一个点是,remove缩容后,需要对elementData的最后的值赋full,避免内层泄露。因为缩容是后面的元素整体向前Copy。

Fail-Fast机制:ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

LinkedList

源码:

  • 数据结构:底层通过双向链表实现
  • 双向链表的每个节点用内部类Node表示。
  • LinkedList通过firstlast引用分别指向链表的第一个和最后一个元素。
  • getFirst(), getLast():直接拿。已经缓存在变量first和last上了。为空时报错。
  • removeFirst():注意手动赋值first.next为null,以帮助GC。
  • removeLast():注意手动赋值last.prev为null,以帮助GC
  • remove(e):删除第一次出现的这个元素,没有则返回false。判断的依据是equals。由于LinkedList可存放null元素,也可以删除null。
  • remove(index):判断改下标有无元素,有的话直接unlink()即可。
  • **add(E e)**:直接加到末尾。
  • add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。
  • addAll(index, c):考虑到效率问题,不是一个一个加。而是直接在最后面继续构造链表。
  • clear():为了让GC更快可以回收放置的元素,需要将node之间的引用关系赋空。

HashMap

HashSet里面有一个HashMap(适配器模式),所以底层是HashMap实现。key是元素,value全部用一个final object填充。

HashMap的key和value都可以放null。

初始容量(inital capacity,默认16)和负载系数(load factor,决定什么时候扩容,默认0.75)。

如果要将自定义的对象放入到HashMapHashSet中,需要**@Override** hashCode()equals()方法。

table.length必须是2的指数:是为了hash(k)&(table.length-1)等价于hash(k)%table.length。提升性能。原因是HashMap要求table.length必须是2的指数,因此table.length-1就是二进制低位全是1,跟hash(k)相与会将哈希值的高位全抹掉,剩下的就是余数了。

源码:

  • 数据结构:数组 + 链表 + 红黑树。元素用Node内部类,有 key,value,hash 和 next 四个属性。红黑树时转为TreeNode。可根据第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树。
  • get()方法:
    1. 找到buckets。(用当N是2的倍数时, M & (N-1) 等价 M % N ,简化了运算)。
    2. 判断是链表还是红黑树,遍历找到元素,根据equals判断相等,返回。
  • put()方法:
    1. 查找有没有该元素,有则直接返回。
    2. 如果还没有,则使用头插法,将元素插入头部。
    3. 插入前,会考虑是否需要扩容并重新Hash。
  • 扩容方法:
  • remove()方法:
    1. 先找到key值对应的entry
    2. 删除该entry(修改链表的相应引用)。

put()方法详解

  1. 数组为空?扩容,默认扩容到16
  2. 第一个元素相等?先标记,最后根据入参,决定是否覆盖。返回倒是都是返回旧值。
  3. 第一个元素为TreeNode?调用红黑树遍历
  4. 插入链表最后面。插入的是第8个?链表转为红黑树。
  5. 链表中找到相等的Node?先标记,最后根据入参,决定是否覆盖。返回倒是都是返回旧值。
  6. 最后整体判断Size,看是否超过阈值threshold需要扩容。

resize()扩容方法详解

  1. 获取容量
    1. new HashMap() 后的第一次put():取默认容量、默认阈值。
    2. new HashMap(int x) 后的第一次put():将阈值直接赋值给容量,此时二者相等。阈值是构造方法设置的,取第一个大于x的2的指数。
    3. 普通扩容:容量、阈值各 * 2。
  2. 扩容
    1. 桶内只有1个元素:直接copy。
    2. 桶内为红黑树:调用红黑树的split()方法分裂到新数组。
    3. 桶内为链表:依次重新计算桶位置,分裂为2个链表。假设老容器桶位置为j,则两个链表依次存入 j 和 capacity + j 位置。

红黑树算法?后续补充。

8.IO(简略)

五大 I/O 模型比较

  • 阻塞
  • 非阻塞
  • I/O多路复用
  • 信号驱动
  • 异步

image-20220630213452270

9.日期和时间(略)

10.正则(简介)

元字符:

代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

反义:

代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

重复:

代码/语法 说明
* 前面的子串,重复0到N次
+ 前面的子串,重复1到N次
? 前面的子串,重复0次或1次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

再加1个:“[]”表示字符集,用于匹配多项。[a-z]表示匹配任意a-z字符

举例:

匹配邮箱:

“abcd test@runoob.com 1234”;

匹配:/\b[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,6}\b/g

image-20210303152741639

11.加密(暂略)

12.函数式编程

在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:

  • Comparator
  • Runnable
  • Callable

Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例,以匿名类方式编写如下:

String[] array = ...
Arrays.sort(array, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:

// Lambda import java.util.Arrays; 

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, (s1, s2) -> {
            return s1.compareTo(s2);
        });
        System.out.println(String.join(", ", array));
    }
}

1.FunctionalInterface

我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

再来看Comparator接口:

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2)其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface

使用Lambda表达式,我们就可以不必编写FunctionalInterface接口的实现类,从而简化代码。

2.方法引用

实例方法、静态方法引用

实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, String::compareTo);	// 注意到这里的方法引用
        System.out.println(String.join(", ", array));
    }
}

构造方法引用

除了可以引用静态方法和实例方法,我们还可以引用构造方法。构造方法的引用写法是类名::new

我们来看一个例子:如果要把一个List<String>转换为List<Person>,应该怎么办?

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

List<String> names = List.of("Bob", "Alice", "Tim");

// 传统的做法是先定义一个ArrayList<Person>,然后用for循环填充这个List:
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
    persons.add(new Person(name));
}

// 要更简单地实现String到Person的转换,我们可以引用Person的构造方法:
public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("Bob", "Alice", "Tim");
        List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
        System.out.println(persons);
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "Person:" + this.name;
    }
}

3.使用Stream

Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。

划重点:这个Stream代表的是任意Java对象的序列。*Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的*。

Stream跟List稍微有点像,都是序列、都存东西,但是又完全不一样。它存的是规则,且是惰性计算的,存的东西不一定已经在内存里了。比如Stream可以存储所有的自然数,而List则不能。

Stream的特点

  • 惰性计算它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。
  • 只转换规则:一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生*。

因此,Stream API的基本用法就是:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:

int result = createNaturalStream() // 创建Stream
             .filter(n -> n % 2 == 0) // 任意个转换
             .map(n -> n * n) // 任意个转换
             .limit(100) // 任意个转换
             .sum(); // 最终计算结果
1.创建Stream

(1)Stream.of静态方法

创建Stream最简单的方式,传入可变参数即可。很少用于生产,用于测试倒是很方便。

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        // forEach()方法相当于内部循环调用,
        // 可传入符合Consumer接口的void accept(T t)的方法引用:
        stream.forEach(System.out::println);
    }
}

(2)基于数组或Collection

  • 数组:Arrays.stream()静态方法
  • Collection:直接调用Collection.stream()方法
public class Main {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
        Stream<String> stream2 = List.of("X", "Y", "Z").stream();
        stream1.forEach(System.out::println);
        stream2.forEach(System.out::println);
    }
}

(3)基于Supplier

通过Stream.generate()方法,需传入一个Supplier对象。

Stream<String> s = Stream.generate(Supplier<String> sp);

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。例如:

public class Main {
    public static void main(String[] args) {
        Stream<Integer> natual = Stream.generate(new NatualSupplier());
        // 注意:无限序列必须先变成有限序列再打印。用到了limit(int n)方法。
        natual.limit(20).forEach(System.out::println);
    }
}

class NatualSupplier implements Supplier<Integer> {
    int n = 0;
    public Integer get() {
        n++;
        return n;
    }
}

(4)其他方法

通过一些API提供的接口,直接获得Stream。例如:

  • Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容。
  • 正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组。

(5)基本类型

Stream<>只能保存基本类型的包装类。为了提高性能,减少拆装包,Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别。

// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
2.使用map

Stream.map()Stream最常用的一个转换方法,它*把一个Stream转换为另一个Stream*。

所谓*map操作,就是把一种操作运算,映射到一个序列的每一个元素上*。

map()方法接收的对象是Function接口对象,返回值是另一个stream对象。

map()方法定义:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function定义:

@FunctionalInterface
public interface Function<T, R> {
    // 将T类型转换为R:
    R apply(T t);
}

例如,对x计算它的平方,可以使用函数f(x) = x * x

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
3.使用filter

Stream.filter()Stream的另一个常用转换方法。

对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream

例如,过滤奇数:

public class Main {
    public static void main(String[] args) {
        IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 != 0)
                .forEach(System.out::println);
    }
}
4.使用reduce

map()filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

举例:从1加到10:

public class Main {
    public static void main(String[] args) {
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
        System.out.println(sum); // 45
    }
}

reduce()方法声明:stream.reduce(初始值,function(上次计算值,本次入参))

reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次运算的结果和本次的元素进行运算,并返回运算的结果:

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}

如果去掉初始值,我们会得到一个Optional<Integer>

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
// Stream的元素有可能是0个,因此返回Optional对象,需要进一步判断结果是否存在。
if (opt.isPresent()) {
    System.out.println(opt.get());
}

举例,将配置文件的每一行配置通过map()reduce()操作聚合成一个Map<String, String>

public class Main {
    public static void main(String[] args) {
        // 按行读取配置文件:
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    return Map.of(ss[0], ss[1]);
                })
                // 把所有Map聚合到一个Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                    m.putAll(kv);
                    return m;
                });
        // 打印结果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });
    }
}
5.输出集合

我们介绍了Stream的几个常见操作:map()filter()reduce()。这些操作对Stream来说可以分为两类,

  • 转换操作,即把一个Stream转换为另一个Stream,例如map()filter()
  • 聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()

转换操作并不会触发任何计算,聚合方法会立刻对Stream进行计算。

对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。

(1)输出为List

Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象。它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

(2)输出为数组

调用toArray()方法,并传入数组的“构造方法”即可:

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

(3)输出为Map

对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        // 把元素s映射为key:
                        s -> s.substring(0, s.indexOf(':')),
                        // 把元素s映射为value:
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}

(4)分组输出

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

假设有这样一个Student类,包含学生姓名、班级和成绩:

class Student {
    int gradeId; // 年级
    int classId; // 班级
    String name; // 名字
    int score; // 分数
}

如果我们有一个Stream<Student>,利用分组输出,可以非常简单地按年级或班级把Student归类。

6.其他操作

我们把Stream提供的操作分为两类:转换操作和聚合操作。除了前面介绍的常用操作外,Stream还提供了一系列非常有用的方法

  • 转换操作:map()filter()sorted()distinct()
  • 合并操作:concat()flatMap()
  • 并行处理:parallel()
  • 聚合操作:reduce()collect()count()max()min()sum()average()
  • 其他操作:allMatch(), anyMatch(), forEach()

以下分别举例:

排序sorted()

Stream的元素进行排序十分简单,只需调用sorted()方法,传入指定的Comparator即可:

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Orange", "apple", "Banana")
            .stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(list);
    }
}
去重distinct()

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct()

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]
截取limit()

常用于把一个无限的Stream转换成有限的Streamskip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:

List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳过A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]

截取操作也是一个转换操作,将返回新的Stream

合并concat()

将两个Stream合并为一个Stream可以使用Stream的静态方法concat()

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
flatMap

如果Stream的元素是集合:

Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));

而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()

Stream<Integer> i = s.flatMap(list -> list.stream());

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream

并行parallel()

把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:

Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
                   .sorted() // 可以进行并行排序
                   .toArray(String[]::new);

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。

其他聚合方法count()max()allMatch()

除了reduce()collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {
    System.out.println("Hello, " + str);
});

13.其他(略)

集合、IO、日期与时间、单元测试、正则、加密与安全、(多线程)、网络编程、JackSon、JDBC。


转载请注明来源