并非Java开发要掌握的全部语法,我挑着来的(提前声明


还是廖大的Java教程

反射

反射Reflection可以在程序运行期,对某个实例一无所知的情况下 拿到一个对象的所有信息,调用其方法

是十分强大的语言武器

Class&动态加载

  • class由JVM在执行过程中动态加载,JVM第一次读取到一种class类型时 将其加载入内存(不遇到不会预先加载,利用这一点可以做到运行期根据条件加载不同的实现类),每加载一种class,JVM就为其创建一个Class类型的实例 并与其关联起来
// class类型: 名叫Class的class
public final class Class{
	private Class(){}
}

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

Class cls = new Class(String);

这个Class实例的构造方法为private,只有JVM可以创建

  • JVM持有的每个Class实例都指向一个数据类型(class or interface)

image-20220227104610572

  • 一个Class实例包含了该class的所有信息,所以如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息,这就叫反射Reflection

image-20220227104646646

  • 获取一个classClass实例的三种方法
// 通过一个class的静态变量class获取
// 已经加载某个类 获取它的java.lang.Class对象
Class cls = String.class;
// 通过实例变量的getClass方法获取
// 上下文已存在某个类的实例
String s = "Hello";
Class cls = s.getClass();
// 通过静态方法Class.forName获取
// 前提是知道class的完整类名
Class cls = Class.forName("java.lang.String");
  • 因为Class实例在JVM中是唯一的,所以上述方法获取的Class实例是同一个实例,可以用==比较
  • 一般应该用instanceof判断数据类型,不但匹配指定类型,还匹配指定类型的子类;而用==判断class实例可以精确地判断数据类型,但不能作子类型比较
Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类

boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
  • 通过反射获取实例的基本信息的小栗子
public class Main {
    public static void main(String[] args) {	// 注
        printClassInfo("".getClass());
        printClassInfo(Runnable.class);
        printClassInfo(java.time.Month.class);
        printClassInfo(String[].class);
        printClassInfo(int.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());
    }
}
  • String[]也是一种类,不同于String.class,它的类名是[Ljava.lang.String;

  • 每一种基本类型对应的Class实例可以用基本类型.class访问

  • 获取到Class实例后可以用它来创建对应类型的实例,与直接new的相比,局限在于只能调用public的无参数构造方法,而有参数的构造方法 或非public的构造方法都无法通过Class.newInstance()调用

Class cls = String.class;
String s = (String) cls.newInstance();
// 相当于 new String()

访问字段

  • Class类提供了以下几个方法来获取字段的(返回Field对象
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 根据字段名获取某个public的field(包括父类)
        System.out.println(stdClass.getField("score"));	// 获取public字段"score"
        System.out.println(stdClass.getField("name"));	// 获取继承的public字段"name"
        // 根据字段名获取当前类的某个field(不包括父类)
        System.out.println(stdClass.getDeclaredField("grade"));	// 获取private字段"grade"
    }
}

class Student extends Person {
    public int score;
    private int grade;
}

class Person {
    public String name;
}
  • 一个Field对象包含一个字段的所有信息
Field f = String.class.getDeclaredField("value");
// 返回字段名称
f.getName(); // "value"
// 返回字段类型 是一个Class实例(比如String.class)
f.getType(); // class [B 表示byte[]类型
// 返回字段的修饰符 是一个int 不同的bit有不同的含义
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.get(Object)可以获取指定实例指定字段的值;如果字段为private可以调用Field.setAccessible(true),一律访问,可能会失败的原因是JVM运行期SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),保护JVM核心库的安全
  • 同样的,用Field.set(Object, Object)可以设置字段的值,第一个Object参数是指定的实例,第二个Object参数是待修改的值
import java.lang.reflect.Field;

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);	// 修改非public字段
        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;
    }
}

调用方法

  • Class类提供了以下几个方法来获取方法的(返回Method对象
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取某个public的Method(包括父类)
        System.out.println(stdClass.getMethod("getScore", String.class));	// 获取public方法getScore,参数为String
        System.out.println(stdClass.getMethod("getName"));	// 获取继承的public方法getName,无参数
        // 获取当前类的某个Method(不包括父类)
        System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));	// 获取private方法getGrade,参数为int
    }
}

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对象包含一个方法的所有信息,函数基本同上一个三级标题
  • 得到Method后我们可以对他进行调用,方式是调用其invoke,第一个参数是对象实例,后面为可变参数,与方法参数一致
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        String s = "Hello world";	// String对象
        Method m = String.class.getMethod("substring", int.class);	        // 获取String substring(int)
        String r = (String) m.invoke(s, 6);	// 调用
        System.out.println(r);
    }
}

值得注意的是,substring自身有两个重载方法,我们获取的是其中的substring(int)这个方法

  • 如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null
import java.lang.reflect.Method;

public class Main{
	public static void main(String[] args)
		Method m = Integer.class.getMethod("parseInt", String.class);	// 获取Integer.parseInt(String)
		Integer n = (Integer) m.invoke(null, "12345");	// 调用
		System.out.println(n);
}
  • 同上一个三级标题对于private字段的处理,我们可以用Method.setAccessible(true),也存在同样可能失败的原因,不重复了
import java.lang.reflect.Method;

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;
    }
}
  • 对于参数不同的方法 我们可以指定参数类型和数量,而对于多态,仍表现出多态的原则,即 总是调用实际类型的覆写方法(如果存在
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Method h = Person.class.getMethod("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() {	// 继承自Person 覆写hello方法
        System.out.println("Student:hello");
    }
}

调用构造方法

  • 正常的new会触发构造方法,通过class的new会有局限性(见上),为了调用任意的构造方法,我们可以使用反射的Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例;Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例
import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取某个public的Constructor
        Constructor cons1 = Integer.class.getConstructor(int.class);	// 获取构造方法Integer(int)
        Integer n1 = (Integer) cons1.newInstance(123);	// 调用构造方法
        System.out.println(n1);

        Constructor cons2 = Integer.class.getConstructor(String.class);	// 获取构造方法Integer(String)
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}
  • 注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题
  • 同上面,可以有setAccessible(true)来调用非public的Constructor

获取继承关系

  • 最开始的三级标题Class中提到了三种方式获取Class实例,他们都是同一个实例,因为JVM对每个加载的Class只创建一个Class实例来表示它的类型
  • 获取父类Class
public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}
  • 获取接口,getInterfaces只返回当前类直接实现的接口类型,不包括父类;如果一个类没有实现任何interface,那么getInterfaces返回空数组
import java.lang.reflect.Method;

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);
        }
    }
}
  • 当我们判断一个实例是否是某个类型时,正常情况下使用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

动态代理

  • class可以实例化而interface不可以,所有interface类型的变量总是通过某个实例向上转型并赋值给接口类型变量
CharSequence cs = new StringBuilder();
  • 静态方式
// 定义接口
public interface Hellp{
	void morning(String name);
}

// 编写实现类
public class HelloWorld implements Hellp{
	public void morning(String name){
		System.out.println("Good morning, " + name);
	}
}

// 创建实例 转型为接口并调用
Hello hello = new HelloWorld();
hello.morning("Bob");
  • 动态代理Dynamic Proxy可以在运行期动态创建某个interface的实例,不编写实现类,直接通过Proxy.newProxyInstance()创建一个Hello接口对象
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() {	// 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 mornig, " + args[0]);
                }
                return null;
            }
        };

        Hello hello = (Hello) Proxy.newProxyInstance(	// 返回的Object强制转型为接口
                Hello.class.getClassLoader(),	// 使用的ClassLoader 通常为接口类的ClassLoader
                new Class[]{Hello.class},	// 需要实现的接口数组 至少传入一个接口
                handler);	// 用来处理接口方法调用的InvocationHandler实例
        hello.morning("Bob");

    }
}


interface Hello{
    void morning(String name);
}
  • 动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,上面的改写为静态实现是这样的
public class HelloDynamicProxy implements Hello{
	InvationHandler 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直接编写了这个中间的类,不需要源码,可以直接生成字节码

注解

注解Annotation,是放在Java源码的类、方法、字段、参数前的一种特殊注释

使用&定义注解

注解可分为三类

  • 1 编译器使用的注解:这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了

@Override:让编译器检查该方法是否正确地实现了覆写

@SuppressWarnings:告诉编译器忽略此处代码产生的警告

  • 2 由工具处理.class文件使用的注解:有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能;这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中
  • 3 程序运行期可读取的注解:加载后一直存在于JVM中,这也是最常用的注解

@PostConstruct:配置了它的方法会在调用构造方法后自动被调用,这是Java代码读取该注解实现的功能,JVM并不会识别该注解

  • 定义一个注解时,还可以定义配置参数,配置参数可以包括:基本类型,String,枚举类,基本类型、String、Class以及枚举的数组;可以设置缺省值,如果只写注解,相当于全部使用默认值
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就是一个注解

  • 有一些注解可以修饰其他注解,这些注解就称为元注解Meta annotation

@Target:使用@Target可以定义Annotation能够被应用于源码的哪些位置

类或接口:ElementType.TYPE
字段:ElementType.FIELD
方法:ElementType.METHOD
构造方法:ElementType.CONSTRUCTOR
方法参数:ElementType.PARAMETER

@Retention:定义了Annotation的生命周期,通常我们自定义的Annotation都是RUNTIME

仅编译期:RetentionPolicy.SOURCE	编译器被丢掉 一般不用(编译器使用)
仅class文件:RetentionPolicy.CLASS	仅保存在class文件中 不会被加载入JVM(底层工具库使用 涉及class的加载)
运行期:RetentionPolicy.RUNTIME	会被加载进JVM 并在运行期被程序读取(常用)

@Repeatable:定义Annotation是否可重复

@Inherited:定义子类是否可继承父类定义的Annotatio,仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效

  • 使用@interface语法来定义注解,用元注释来配置注释,其中@Target@Retention(一般设为RUNTIME)必须设置
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

处理注解

注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定

注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,所以读取注解需要用到反射

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

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);

反射API读取Annotation:

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

使用反射API读取Annotation有两种方法。方法一是先判断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) {
   ...
}

读取方法、字段和构造方法的Annotation和Class类似。但要读取方法参数的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;
    }
}

定义了注解,本身对程序逻辑没有任何影响,必须自己编写代码来使用注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}

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不会自动给注解添加任何额外的逻辑。

IO

File&Path

  • 标准库java.io提供File对象来操作文件和目录,可以传入绝对路径和相对路径;即使传入的不存在也不报错,可以用isFile, isDirectory进行检查
  • File对象既可以表示文件,也可以表示目录,可以用getPath(传入路径), getAbsolutePath(绝对路径), getCanonicalPath(规范路径)
  • 当File对象表示一个目录时,可以使用list()listFiles()列出目录下的文件和子目录名,listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录
import java.io.*;
public class Main {
    public static void main(String[] args) throws IOException {
        File f = new File("C:\\Windows");
        File[] fs1 = f.listFiles(); // 列出所有文件和子目录
        printFiles(fs1);
        File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
            public boolean accept(File dir, String name) {
                return name.endsWith(".exe"); // 返回true表示接受该文件
            }
        });
        printFiles(fs2);
    }

    static void printFiles(File[] files) {
        System.out.println("==========");
        if (files != null) {
            for (File f : files) {
                System.out.println(f);
            }
        }
        System.out.println("==========");
    }
}
  • 其它API
boolean canRead():是否可读
boolean canWrite():是否可写
boolean canExecute():是否可执行
long length():文件字节大小

boolean mkdir():创建当前File对象表示的目录
boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来
boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功
  • 可以用createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
        f.deleteOnExit(); // JVM退出时自动删除
        System.out.println(f.isFile());
        System.out.println(f.getAbsolutePath());
    }
}
  • Path对象位于java.nio.file包,和File对象类似,但操作更加简单,便于操作目录的拼接、遍历等
import java.io.*;
import java.nio.file.*;

public class Main {
    public static void main(String[] args) throws IOException {
        Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
        System.out.println(p1);
        Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
        System.out.println(p2);
        Path p3 = p2.normalize(); // 转换为规范路径
        System.out.println(p3);
        File f = p3.toFile(); // 转换为File对象
        System.out.println(f);
        for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
            System.out.println("  " + p);
        }
    }
}

Files&Paths

java.nio包里面的类,封装了很多读写文件的简单方法

// 把一个文件的全部内容读取为一个byte[]
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
// 把一个文件的全部内容读取为String
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));
// 写入文件
// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);

注意,Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容

字节流读写

  • InputStream代表输入字节流,OuputStream代表输出字节流,以byte为最小单位

  • ReaderWriter表示字符流(读写字符,并且字符不全是单字节表示的ASCII字符),以char为最小单位

  • 如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些

  • InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类,这个抽象类定义的一个最重要的方法就是int read()

public abstract int read() throws IOException;

这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255),如果已读到末尾,返回-1表示不能继续读取了

  • FileInputStreamInputStream的一个子类,从文件流中读取数据
public void readFile() throws IOException{
	InputStream input = new FileInputStream("src/readme.md");
	for(;;){
		int n = input.read();
		if(n == -1){
			break;
		}
		System.out.println(n);
	}
	input.close();
}
  • 所有与IO操作相关的代码都必须正确处理IOException,可以用try ... finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭
public void readFile() throws IOException{
	InputStream input = null;
	try{
		input = new FileInputStream("src/readme.txt");
		int n;
		while((n = input.read())!=-1){
			System.out.prinln(n);
		}
	}finally{
		if(input!=null){
			input.close();
		}
	}
}
  • java7之后可以用更简单的try(resource)
public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 编译器在此自动为我们写入finally并调用close()
}

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此都可以用在try(resource)

  • InputStream提供了两个重载方法来支持读取多个字节
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了

利用缓冲区一次读取多个字节的代码如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        // 定义1000个字节大小的缓冲区:
        byte[] buffer = new byte[1000];
        int n;
        while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
            System.out.println("read " + n + " bytes.");
        }
    }
}
  • FileInputStream以外还有ByteArrayInputStream可以在内存中模拟一个InputStream,实际上是把一个byte[]数组在内存中变成一个InputStream
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = { 72, 101, 108, 108, 111, 33 };
        try (InputStream input = new ByteArrayInputStream(data)) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
        }
    }
}
  • OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地

字符流读写

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)

除了特殊的CharArrayReaderStringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream

既然Reader本质上是一个基于InputStreambytechar的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:

// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。上述代码可以通过try (resource)更简洁地改写如下:

try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
    // TODO:
}

上述代码实际上就是FileReader的一种实现方式。

使用try (resource)结构时,当我们关闭Reader时,它会在内部自动调用InputStreamclose()方法,所以,只需要关闭最外层的Reader对象即可

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
无对应方法写入String:void write(String s)

除了CharArrayWriterStringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
    // TODO:
}

上述代码实际上就是FileWriter的一种实现方式,这和上面的InputStreamReader是一样的

Filter模式

Filter模式可以在运行期动态增加功能,又称Decorator模式

classpath

从classpath读取文件就可以避免不同环境下文件路径不一致的问题,在classpath中的资源文件,路径总是以开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:

try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
    // TODO:
}

调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到:

try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
    if (input != null) {
        // TODO:
    }
}

如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));

这样读取配置文件,应用程序启动就更加灵活

序列化&反序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口(标记接口Marker Interface)

public interface Serializable{
}

反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行(同PHP),可设置serialVersionUID作为版本号

public class Person implements Serializable {
    private static final long serialVersionUID = 2709425275741743919L;
}

可以避免class定义变动导致的不兼容,通常可以由IDE自动生成

网络编程

Socket - TCP

一个Socket就是由ip和port构成,Socket编程就是指两个进程之间的网络通信,Server&Client

// Server.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;


public class Server {
    public static void main(String[] args) throws IOException{
        ServerSocket ss = new ServerSocket(6980);
        System.out.println("Server is running...");
        for(;;){
            Socket sock = ss.accept();
            System.out.println("connected from " + sock.getRemoteSocketAddress());
            Thread t = new Handler(sock);
            t.start();
        }
    }
}


class Handler extends Thread{
    Socket sock;
    public Handler(Socket sock){
        this.sock = sock;
    }
    @Override
    public void run(){
        try(InputStream input = this.sock.getInputStream()){
            try(OutputStream output = this.sock.getOutputStream()){
                handle(input, output);
            }
        }catch (Exception e){
            try {
                this.sock.close();
            }catch (IOException ioe){
            }
            System.out.println("Client disconnected");
        }
    }

    private void handle(InputStream input, OutputStream output) throws IOException{
        var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        writer.write("hello\n");
        writer.flush();
        for (;;){
            String s = reader.readLine();
            if(s.equals("bye")){
                writer.write("bye~\n");
                writer.flush();
                break;
            }
            writer.write("ok: " + s + "\n");
            writer.flush();
        }
    }
}
public class Client {
    public static void main(String[] args) throws IOException {
        Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口
        try (InputStream input = sock.getInputStream()) {
            try (OutputStream output = sock.getOutputStream()) {
                handle(input, output);
            }
        }
        sock.close();
        System.out.println("disconnected.");
    }

    private static void handle(InputStream input, OutputStream output) throws IOException {
        var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server] " + reader.readLine());
        for (;;) {
            System.out.print(">>> "); // 打印提示
            String s = scanner.nextLine(); // 读取一行输入
            writer.write(s);
            writer.newLine();
            writer.flush();
            String resp = reader.readLine();
            System.out.println("<<< " + resp);
            if (resp.equals("bye")) {
                break;
            }
        }
    }
}

注意flush(),不然可能都收不到消息

Socket - UDP

Server

DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
for (;;) { // 无限循环
    // 数据缓冲区:
    byte[] buffer = new byte[1024];
    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
    ds.receive(packet); // 收取一个UDP数据包
    // 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
    // 将其按UTF-8编码转换为String:
    String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
    // 发送数据:
    byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
    packet.setData(data);
    ds.send(packet);
}

接收UDP数据包之前先要准备缓冲区,并通过DatagramPacket实现接收

假设我们收取到的是一个String,那么,通过DatagramPacket返回的packet.getOffset()packet.getLength()确定数据在缓冲区的起止位置

当服务器收到一个DatagramPacket后,通常必须立刻回复一个或多个UDP包,因为客户端地址在DatagramPacket中,每次收到的DatagramPacket可能是不同的客户端,如果不回复,客户端就收不到任何UDP包

Client

DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
// 发送:
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
ds.disconnect();

这里的connect不是真连接,它是为了在客户端的DatagramSocket实例中保存服务器端的IP和端口号,确保这个DatagramSocket实例只能往指定的地址和端口发送UDP包,不能往其他地址和端口发送。这么做不是UDP的限制,而是Java内置了安全检查。

如果客户端希望向两个不同的服务器发送UDP包,那么它必须创建两个DatagramSocket实例。

后续的收发数据和服务器端是一致的。通常来说,客户端必须先发UDP包,因为客户端不发UDP包,服务器端就根本不知道客户端的地址和端口号。

disconnect()也不是真正地断开连接,它只是清除了客户端DatagramSocket实例记录的远程服务器地址和端口号,这样,DatagramSocket实例就可以连接另一个服务器端。

HttpClient

URL url = new URL("http://www.example.com/path/to/target?a=1&b=2");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setConnectTimeout(5000); // 请求超时5秒
// 设置HTTP头:
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");
// 连接并发送HTTP请求:
conn.connect();
// 判断HTTP响应是否200:
if (conn.getResponseCode() != 200) {
    throw new RuntimeException("bad response");
}
// 获取所有响应Header:
Map<String, List<String>> map = conn.getHeaderFields();
for (String key : map.keySet()) {
    System.out.println(key + ": " + map.get(key));
}
// 获取响应内容:
InputStream input = conn.getInputStream();
...

从Java 11开始,引入了新的HttpClient

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.*;

public class Main {
    // 全局HttpClient:
    static HttpClient httpClient = HttpClient.newBuilder().build();

    public static void main(String[] args) throws Exception {
        String url = "https://www.sina.com.cn/";
        HttpRequest request = HttpRequest.newBuilder(new URI(url))
            // 设置Header:
            .header("User-Agent", "Java HttpClient").header("Accept", "*/*")
            // 设置超时:
            .timeout(Duration.ofSeconds(5))
            // 设置版本:
            .version(Version.HTTP_2).build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        // HTTP允许重复的Header,因此一个Header可对应多个Value:
        Map<String, List<String>> headers = response.headers().map();
        for (String header : headers.keySet()) {
            System.out.println(header + ": " + headers.get(header).get(0));
        }
        System.out.println(response.body().substring(0, 1024) + "...");
    }
}

post

String url = "http://www.example.com/login";
String body = "username=bob&password=123456";
HttpRequest request = HttpRequest.newBuilder(new URI(url))
    // 设置Header:
    .header("Accept", "*/*")
    .header("Content-Type", "application/x-www-form-urlencoded")
    // 设置超时:
    .timeout(Duration.ofSeconds(5))
    // 设置版本:
    .version(Version.HTTP_2)
    // 使用POST并设置Body:
    .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String s = response.body();

RMI

一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法

要实现RMI,服务器和客户端必须共享同一个接口interface,测试

public interface WorldClock extends Remote {
    LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}

此接口必须派生自java.rmi.Remote,并在每个方法声明抛出RemoteException

下一步是编写服务器的实现类,因为客户端请求的调用方法getLocalDateTime()最终会通过这个实现类返回结果

public class WorldClockService implements WorldClock{
    @Override
    public LocalDataTime getLocalDateTime(String zoneId) throws RemoteException{
        return LocalDataTime.now(ZoneId.of(zoneId)).withNano(0);
    }
}

现在需要通过Java RMI提供的一系列底层支持接口,把上面编写的服务以RMI的形式暴露在网络上,客户端才能调用

public class Server {
    public static void main(String[] args) throws RemoteException {
        System.out.println("create World clock remote service...");
        // 实例化一个WorldClock:
        WorldClock worldClock = new WorldClockService();
        // 将此服务转换为远程服务接口:
        WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0);
        // 将RMI服务注册到1099端口:
        Registry registry = LocateRegistry.createRegistry(1099);
        // 注册此服务,服务名为"WorldClock":
        registry.rebind("WorldClock", skeleton);
    }
}

由于RMI要求服务器和客户端共享同一个接口,因此我们要把WorldClock.java这个接口文件复制到客户端,然后在客户端实现RMI调用

public class Client {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        // 连接到服务器localhost,端口1099:
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 查找名称为"WorldClock"的服务并强制转型为WorldClock接口:
        WorldClock worldClock = (WorldClock) registry.lookup("WorldClock");
        // 正常调用接口方法:
        LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai");
        // 打印调用结果:
        System.out.println(now);
    }
}

对客户端来说,客户端持有的WorldClock接口实际上对应了一个“实现类”,它是由Registry内部动态生成的,并负责把方法调用通过网络传递到服务器端。而服务器端接收网络调用的服务并不是我们自己编写的WorldClockService,而是Registry自动生成的代码。我们把客户端的“实现类”称为stub,而服务器端的网络服务类称为skeleton,它会真正调用服务器端的WorldClockService,获取结果,然后把结果通过网络传递给客户端

RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务

整个过程由RMI底层负责实现序列化和反序列化,很容易产生安全问题


其它的语法等遇到了再学,估计还会有个Java语法补全下篇(如果我不懒的话