并非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)
- 一个
Class
实例包含了该class
的所有信息,所以如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息,这就叫反射Reflection
- 获取一个
class
的Class
实例的三种方法
// 通过一个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
可能不允许对java
和javax
开头的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
,所以读取注解需要用到反射
判断某个注解是否存在于Class
、Field
、Method
或Constructor
:
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为最小单位Reader
和Writer
表示字符流(读写字符,并且字符不全是单字节表示的ASCII字符),以char为最小单位如果数据源不是文本,就只能使用
InputStream
,如果数据源是文本,使用Reader更方便一些InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类,这个抽象类定义的一个最重要的方法就是int read()
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int
值(0~255),如果已读到末尾,返回-1
表示不能继续读取了
FileInputStream
是InputStream
的一个子类,从文件流中读取数据
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()
方法。InputStream
和OutputStream
都实现了这个接口,因此都可以用在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()
方法,它的目的是将缓冲区的内容真正输出到目的地
字符流读写
InputStream | Reader |
---|---|
字节流,以byte 为单位 | 字符流,以char 为单位 |
读取字节(-1,0~255):int read() | 读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
除了特殊的CharArrayReader
和StringReader
,普通的Reader
实际上是基于InputStream
构造的,因为Reader
需要从InputStream
中读入字节流(byte
),然后,根据编码设置,再转换为char
就可以实现字符流。如果我们查看FileReader
的源码,它在内部实际上持有一个FileInputStream
。
既然Reader
本质上是一个基于InputStream
的byte
到char
的转换器,那么,如果我们已经有一个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
时,它会在内部自动调用InputStream
的close()
方法,所以,只需要关闭最外层的Reader
对象即可
OutputStream | Writer |
---|---|
字节流,以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) |
除了CharArrayWriter
和StringWriter
外,普通的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语法补全下篇(如果我不懒的话