69. Java

一、概览

1. 特点

  1. 面向对象编程;
  2. 字节码方式运行在虚拟机上;
  3. 简单,健壮,安全;
  4. 跨平台。

2. 版本

  1. Java SE:Standard Edition,标准版;
  2. Java EE:Enterprise Edition,企业版;
  3. Java ME:Micro Edition,移动版。

需要注意的是,目前的Android开发并非是该移动版本。

3. 规范

  1. JSR:Java Specification Request,Java规范提案;
  2. JCP:Java Community Process,国际组织;
  3. RI:Reference Implementation,参考实现;
  4. TCK:Technology Compatibility Kit,测试套件;
  5. JDK:Java Development Kit,Java开发套件;
  6. JRE:Java Runtime Environment,Java运行环境。

4. 环境

zhgxun-pro:java zhgxun$ java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
zhgxun-pro:java zhgxun$ javac -version
javac 1.8.0_60
zhgxun-pro:java zhgxun$

5. 数据类型

整数类型:byte,short,int,long

浮点类型:float,double

字符类型:char

布尔类型:boolean

6. 常量

用final定义;

通常全部大写;

避免magic number。

二、Maven

Maven是一个Java项目管理和构建工具,Maven使用pom.xml定义项目内容,并使用预设的目录结构,在Maven中声明一个依赖项可以自动下载并导入classpath,Maven使用groupId,artifactId和version唯一定位一个jar包。

1. Java目录结构

zhgxun-pro:banana zhgxun$ tree
.
├── output.txt
├── pom.xml // 项目描述文件
├── src
│   ├── main
│   │   ├── java // 存放Java源代码
│   │   │   └── github
│   │   │       └── banana
│   │   │           ├── App.java
│   │   │           ├── CountInputStream.java
│   │   └── resources // 存放资源文件, 比如一些配置文件, 图片,  前端模板视图等
│   │       ├── LinuxStory.jpeg
│   │       └── log4j2.xml
│   └── test // 单元测试
│       └── java
│           └── github
│               └── banana
│                   └── AppTest.java
├── target // 编译打包后的文件
│   ├── classes
│   │   ├── LinuxStory.jpeg
│   │   ├── github
│   │   │   └── banana
│   │   │       ├── App.class
│   │   │       ├── CountInputStream.class
│   │   └── log4j2.xml
│   └── test-classes
│       └── github
│           └── banana
│               └── AppTest.class
├── text.txt
└── yii_framework_change_log.tar.gz

zhgxun-pro:banana zhgxun$

2. 依赖管理

Maven通过解析依赖关系确定项目所需的jar包。

常用的4种scope:

  1. compile(默认)编译时需要
  2. test 编译test时需要
  3. runtime 编译时不需要, 但运行时需要
  4. provided 编译时需要,但运行时由JDK提供或由某个服务器提供

Maven从中央仓库下载所需jar包并缓存在本地。

Maven维护了一个中央仓库,第三方将自身上传至中央仓库,Maven从中央仓库将所需要的包下载到本地,并且会自动缓存下载过的jar包,~/.m2/repository。

可以通过镜像加速下载,aliyun镜像仓库配置 ~/.m2/settings.xml:

<settings>
    <mirrors>
        <mirror>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>central</mirrorOf>
        </mirror>
    </mirrors>
</settings>

3. 常用插件

插件名称 对应执行的Phase
clean clean
compile compile
surefile test
jar package

创建一个可执行的jar包,并把运行时需要的jar一起打包到jar包中,需要使用插件 maven-shade-plugin

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>该处修改成自己的main类名</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

cobertura-maven-plugin

生成单元测试覆盖率报告。

findbugs-maven-plugin

对Java源码进行静态分析以找出潜在的问题。

三、IO

1. File

IO: Input / Output。

  1. 从外部读入数据;
  2. 输出内容在某处;
  3. IO流是一种顺序读写数据的模式;
  4. 单向流动,以byte字节为最小单位。

IO流是一种流式的数据输入/输出模型:

  1. 二进制数据以byte为最小单位在InputStream / OutputStream中单向流动;
  2. 字符数据以char为最小单位在Reader / Writer中单向流动;
  3. JDK的java.io包提供了同步IO功能 - 编写简单, CPU执行效率低;
  4. JDK的java.nio包提供了异步IO功能 - 编写复杂, CPU执行效率高。

Java的IO流的接口和实现分离:

  1. 字节流接口:InputStream / OutputStream;
  2. 字符流接口:Reader / Writer。

java.io.File表示文件系统的一个文件或者目录,创建File对象本身不涉及IO操作。

  1. isFile():是否是文件;
  2. isDirectory():是否是目录。

文件操作:

  1. canRead():是否允许读取该文件;
  2. canWrite():是否允许写入该文件;
  3. canExecute():是否允许运行该文件;
  4. length():获取文件大小;
  5. createNewFile():创建一个新文件;
  6. static createTempFile():创建一个临时文件;
  7. delete():删除该文件;
  8. deleteOnExit():在JVM退出时删除该文件。

目录操作:

  1. String[] list():列出目录下的文件和子目录名;
  2. File[] listFiles():列出目录下的文件和子目录名;
  3. File[] listFiles(FileFilter filter);
  4. File[] listFiles(FilenameFilter filter);
  5. mkdir():创建该目录;
  6. mkdirs():创建该目录,并在必要时将不存在的父目录也创建出来;
  7. delete():删除该目录。
// File[] listFiles(FilenameFilter filter) 过滤掉部分目录或文件
System.out.println("只获取以xml结尾的文件");
// 添加一个回调函数处理
File[] files = file.listFiles(new FilenameFilter() {

    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".xml");
    }
});
for (File file : files) {
    System.out.println(file);
}

2. InputStream

java.io.InputStream是所有输入流的超类:

  1. abstruct int read() 读取下一个字节,并且返回字节(0-255),读到末尾返回-1;
  2. int read(byte[] b) 一次读取若干字节并填充到byte数组,返回读取的字节数;
  3. int read(byte[], int off, int len) 指定byte数组的偏移量和最大填充数;
  4. void close() 关闭输入流。

read()方法是阻塞(blocking)的。

使用try(resource)可以保证InputStream正确关闭。

完整读取一个文本的所有字节:

// 按字节读入文件流
System.out.println("按字节读入文件流");
// try () {} 写法能正确的关闭资源, 也是推荐的写法
try (InputStream input = new FileInputStream(
        "src/main/java/github/banana/IO.java")) {
    int n = 0;
    // 设置一个缓存, 一次读入多个字节
    byte[] buffer = new byte[1024];
    // 读入结束返回-1, 并且该方法是阻塞读取的
    while ((n = input.read(buffer)) != -1) {
        System.out.println(n);
    }
}

FileInputStream是InputStream的一个文件实现,可以从文件读取输入流。

ByteArrayInputStream可以在内存中模拟一个InputStream,网络编程会经常使用。

3. OutputStream

java.io.OutputStream是所有输出流的超类:

  1. abstruct write(int b) 写入一个字节;
  2. void write(byte[] b) 写入一个字符数据中的所有字节;
  3. void write(byte[] b, int off, int len) 写入字符数组指定的字节;
  4. void close() 关闭输出流;
  5. void flush() 将缓冲区的内容输出,磁盘,网络处于效率的考虑,先输出至缓冲区,在写入磁盘等位置。

write()方法是阻塞(blocking)的。

使用try(resource)可以保证OutputStream正确关闭。

// OutputStream是所有输出流的超类
System.out.println("OutputStream是所有输出流的超类");
try (OutputStream output = new FileOutputStream("output.txt")) {
    // write(int b)写入一个字节
    // write(byte[])写入byte[]数组的所有字节
    // flush()方法将缓冲器内容输出
    // write()方法是阻塞(blocking)的
    // 通常在调用close()方法前都会刷新缓冲区
    // 字节数据都要处理编码
    byte[] b1 = "你好".getBytes("UTF-8");
    output.write(b1);
    byte[] b2 = "世界,这里是中国,绵延长远的万里长城".getBytes("UTF-8");
    output.write(b2);
    System.out.println("写入完毕");
}

4. Filter模式

Java IO 使用Filter模式为InputStream / OutputStream 增加功能:

  1. 可以把一个InputStream和任意的FilterInputStream组合;
  2. 可以把一个OutputStream和任意的FilterOutputStream组合。

Filter模式可以在运行期动态增加功能, 又称Decorator模式, 通过少量的类实现了各种功能的组合。

InputStream:

  1. FileImputStream FilterInputStream
  2. ByteArrayInputStream BufferedInputStream
  3. ServletInputStream DataInputStream, CheckedInputStream

DigestInputStream 签名;

CipherInputStream 加解密;

GZIPInputStream gzip压缩处理。

CountInputStream.java

package github.banana;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 继承FilterInputStream的方式来新增功能, 统计读取到的字节个数
 * 
 * @author zhgxun
 *
 */
public class CountInputStream extends FilterInputStream {
    // 统计读取到的文本字节数
    public int count;

    public CountInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int n = super.read(b, off, len);
        count += n;
        return n;
    }

}

使用实现:

System.out.println("Filter模式是为了解决子类数量爆炸的问题");
System.out.println("把一个.gz压缩的文件以字符串读取出来");
// 尝试读取一个Yii框架的压缩更新日志记录文件
// 如果想统计读取到的字符个数, 可以新增一个实现类
// 需要注意的是如果此时要打印新增的实现类, 则要用新增类的类型, 不能再用原始的InputStream类型
try (CountInputStream input = new CountInputStream(
        new GZIPInputStream(new BufferedInputStream(new FileInputStream("yii_framework_change_log.tar.gz"))))) {
    // 读入到一个输出缓冲中
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    int n = 0;
    // 一次读取4096个字节
    byte[] bytes = new byte[4096];
    while ((n = input.read(bytes)) != -1) {
        // 将每次读取到的字节写入到缓冲中
        output.write(bytes, 0, n);
    }
    // 将字节转换为字节数组
    byte[] data = output.toByteArray();
    // 将字节数组转换为字符串
    String text = new String(data, "UTF-8");
    System.out.println(text);
    System.out.println("该次读取到的字节个数为:" + input.count);
}

5. classpath

classpath中可以包含任意类型的文件。

从classpath读取文件可以避免不同环境下文件路径不一致的问题。

读取classpath资源:

// 从classpath中读取资源
// 相对于src目录来说是根目录
// 而且要明确指定路径/output.txt
try (InputStream inputStream = IO.class.getResourceAsStream("/output.txt")) {
    // 读取失败返回null
    if (inputStream != null) {
        // 一次读取一行
        BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
        System.out.println(bufReader.readLine());
    } else {
        System.out.println("file not found.");
    }
}

6. Reader

InputStream和Reader的区别

InputStream Reader
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0-255),int read() 读取字符(-1, 0-65536),int read()
读到字节数组 int read(byte[]) 读到字符数组 int read(char[] c)

Reader以字符为最小单位实现了字符流输入,是所有字符输入流的超类:

  1. int read() 读取下一个字符,并返回字符(0-65535),读到末尾返回-1;
  2. int read(char[] c) 读取若干字符并填充到char[]数组,返回读取的字符数;
  3. int read(char[] c, int off, int len) 指定读取数组的偏移量和读取的最大长度;
  4. void close() 关闭Reader。

常用Reader类:

  1. FileReader:从文件读取;
  2. CharArrayReader:从char[]数组读取。

Reader是基于InputStream构造的,FileReader内部持有一个InputStream。任何InputStream都可指定编码并通过InputStreamReader转换为Reader:

System.out.println("一次性尽可能读取更多字符并且转换编码");
try (Reader reader = new InputStreamReader(new FileInputStream("text.txt"), "UTF-8")) {
    char[] chars = new char[4096];
    while (reader.read(chars) != -1) {
        System.out.println(chars);
    }
}

Writer以字符为最小单位实现了字符流输出,可以直接写入字符串。

四、集合

1. java.util包提供了集合类,包括:

  1. Collection:根接口;
  2. List:有序列表;
  3. Set:无重复元素集合;
  4. Map:通过Key查找Value的映射表。

2. Java集合设计的特点:

  1. 接口和实现相分离:ArrayList, LinkedList;
  2. 支持泛型,List users = new ArrayList<>();
  3. 访问集合有统一的方法,如迭代器。

3. List

List是一种有序列表,通过索引访问元素。

  1. void add(E e) 在末尾添加一个元素;
  2. void add(int index, E e) 在指定索引添加一个元素;
  3. int remove(int index) 删除指定索引的元素;
  4. int remove(Object e) 删除某个元素;
  5. E get(int index) 获取指定索引的元素;
  6. int size() 获取链表大小(包含元素的个数)。

List有ArrayList和LinkedList两种实现。

比较的项目 ArrayList LinkedList
获取指定元素 速度很快 需要从头开始查找元素
添加元素到末尾 速度很快 速度很快
在指定位置添加、删除 需要移动元素 不需要移动元素
内存占用 较大

通常情况下,优先使用ArrayList。

遍历List使用Iterator或者foreach循环,迭代需要使用hasNext()方法检查是否到达末尾,使用next()获取下一个元素。编译器本身是不知道迭代对象的,但是会将foreach转换成迭代器方式来调用。

List和Array可以相互转换。

4. equals

判断元素是否存在或者查找元素索引:

  1. boolean contains(Object o) 是否包含某个元素;
  2. int indexOf(Object o) 查找某个元素的索引,不存在返回-1。

要正确调用contains / indexOf方法,放入的实例要正确实现equals()。

equals()编写方法:

  1. 判断this==o;
  2. 判断o instanceof Person;
  3. 强制转型,并比较每个对应的字段,基本类型字段用==直接比较,引用类型字段借助Objects.equals()判断。

如果要在List中查找元素:

  1. List的实现类通过元素的equals()方法比较两个元素;
  2. 放入的元素必须正确覆写equals()方法,JDK提供的String,Integer等已经覆写了equals()方法;
  3. 编写equals()方法可以借助Objects.equals()方法;
  4. 如果不在List中查找元素,可以不用覆写equals()方法。
package test;

import java.util.Objects;

/**
 * 人类
 * 
 * @author zhgxun
 *
 */
public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // 正确使用Map必须保证
    // 作为Key的对象必须正确覆写equals()方法
    // 作为Key的对象必须正确覆写hashCode()方法
    // 
    // 覆写hashCode
    // 如果两个对象相等,则两个对象的hashCode()必须相等
    // 如果两个对象不相等,则两个对象的hashCode()尽量不相等(可以相等,会造成效率下降)
    // hashCode可以通过Objects.hashCode()辅助方法实现

    @Override
    public int hashCode() {
        return Objects.hash(this.name, this.age);
    }

    @Override
    public boolean equals(Object obj) {
        // 相同对象
        if (obj == this) {
            return true;
        }
        // 父对象
        if (obj instanceof Person) {
            Person p = (Person) obj;
            // 使用对象本身提供的比较方法
            return Objects.equals(p.getName(), this.getName()) && p.getAge() == this.getAge();
        }
        return false;
    }

    @Override
    public String toString() {
        return "(Person: " + name + ", " + age + ")";
    }
}

5. Map

Map是一种键值映射表,可以通过Key快速查找Value。

常用方法:

  1. V put(K key, V value):把Key-Value放入Map;
  2. V get(K key):通过Key获取Value;
  3. boolean containsKey(K key):判断Key是否存在。

遍历Map,用for…each循环:

  1. 遍历Key:keySet();
  2. 遍历Key和Value:entrySet()。

常用的实现类:

  1. HashMap:不保证有序;
  2. SortedMap:保证按Key排序,实现类有TreeMap。
// 做一个map
// Map是一种键值映射表,可以通过Key快速查找Value
List<Person> p = Arrays.asList(new Person("张三", 19), new Person("李四", 15), new Person("王五", 30));
// HashMap无序打印, 顺序打印要用TreeMap
// 而且顺序只是对键起作用, 不会操作值
Map<String, Person> map = new TreeMap<>(new Comparator<String>() {

    @Override
    public int compare(String o1, String o2) {
        return -o1.compareTo(o2);
    }

});
for (Person t : p) {
    map.put(t.getName(), t);
}

System.out.println(map.get("张三"));
System.out.println(map.get("田七"));

for (String m : map.keySet()) {
    System.out.println(m + " -> " + map.get(m));
}

for (Map.Entry<String, Person> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

6. Set

Set用于存储不重复的元素集合:

  1. boolean add(E e);
  2. boolean remove(Object o);
  3. boolean contains(Object o);
  4. int size()。

利用Set可以去除重复元素。

放入Set的元素要正确实现equals()和hashCode()。

Set不保证有序:

  1. HashSet是无序的;
  2. TreeSet是有序的。

实现了SortedSet接口的是有序Set。

System.out.println("Set...");
List<String> list1 = Arrays.asList("pear", "apple", "banana", "orange", "apple", "banana");
List<String> list2 = removeDuplicate(list1);
System.out.println(list2);

List<String> list3 = Arrays.asList("abc", "xyz", "abc", "www", "edu", "www", "abc");
Set<String> set = new LinkedHashSet<>(list3);
System.out.println(new ArrayList<String>(set));
/**
 * 去除List中重复的元素
 * 
 * @param list
 * @return
 */
private static List<String> removeDuplicate(List<String> list) {
    // 使用Set的方式存储
    // 利用Set可以去除重复元素
    // HashSet是无序的
    // TreeSet是有序的
    // 实现了SortedSet接口的是有序Set
    // 自定义排序算法
    Set<String> set = new TreeSet<>(new Comparator<String>() {

        @Override
        public int compare(String o1, String o2) {
            return -o1.compareTo(o2);
        }

    });
    set.addAll(list);
    // 把set转化为list返回
    return new ArrayList<String>(set);
}

7. Queue

队列(Queue)是一种先进先出(FIFO)的数据结构。

实现类:ArrayDeque,LinkedList。

操作Queue的元素的方法:

  1. 获取队列的长度:size()
  2. 添加至队尾压栈:add(),失败抛异常, / offer(),失败返回false;
  3. 获取队列头部元素并删除:E remove(),失败抛异常; / E poll(),失败返回null;
  4. 获取队列头部元素但不删除:E element(),失败抛异常; / E peek(),失败返回null;
  5. isEmpty()判断队列是否为空。

两组方法的区别:是否抛出Exception。

避免把null添加到队列。

// 初始化一个队列
Queue<User> users = new LinkedList<>();
// 往队列添加元素
// 添加失败抛异常
users.add(new User(1, "周迅", 18));
// 添加失败返回false
users.offer(new User(2, "袁姗姗", 20));
users.add(new User(3, "张国荣", 30));
users.offer(new User(4, "罗大佑", 20));

System.out.println("队列的长度:" + users.size());
System.out.println("获取头部元素并删除:" + users.remove());
System.out.println("队列的长度:" + users.size());
System.out.println("获取头部元素并删除:" + users.poll());
System.out.println("队列的长度:" + users.size());
System.out.println("获取头部元素不删除:" + users.element());
System.out.println("队列的长度:" + users.size());
System.out.println("获取头部元素不删除:" + users.peek());
System.out.println("队列的长度:" + users.size());

PriorityQueue优先队列:

  1. PriorityQueue的出队顺序与元素的优先级有关,总是返回优先级最高的元素;
  2. 从队首获取元素时,总是获取优先级最高的元素;
  3. 默认按元素比较的顺序排序(必须实现Comparable接口);
  4. 可以通过Comparator自定义排序算法(不必实现Comparable接口)。

优先队列需要实现 Comparable 接口:

@Override
public int compareTo(User o) {
    return o.name.compareTo(this.name);
}

实现自定义的排序方法:

// 实现自定义的排序方法
PriorityQueue<User> users = new PriorityQueue<>(new Comparator<User>() {

    @Override
    public int compare(User o1, User o2) {
        return -o1.getName().compareTo(o2.getName());
    }

});

Deque。

Deque实现一个双端队列(Double Ended Queue):

  1. 既可以添加到队尾,也可以添加到队首;
  2. 既可以从队首获取,又可以从队尾获取。

8. Stack

栈(Stack)是一种后进先出(LIFO)的数据结构。

操作栈的元素的方法:

  1. push()压栈;
  2. pop()出栈;
  3. peek()取栈顶元素但不出栈。

Java使用Deque实现栈的功能,注意只调用push/pop/peek,避免调用Deque的其他方法。

不要使用遗留类Stack。

五、JDBC

JDBC是Java程序访问数据库的标准接口:

  1. JDK提供JDBC接口,数据库厂商提供JDBC驱动(JDBC实现);
  2. Connection代表一个JDBC连接。

maven配置数据库连接组件

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>6.0.6</version>
</dependency>

以事务的方式处理:

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * 事务处理
 * 
 * @author zhgxun
 *
 */
public class DBTx {
    private static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC";
    private static final String JDBC_USER = "root";
    private static final String JDBC_PASSWORD = "";

    public static void main(String[] args) {
        // 打印用户列表
        List<User> users = null;
        try {
            users = getUsers();
            for (User user : users) {
                System.out.println(user);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        // 开启更新事务
        System.out.println("开启更新事务");
        Connection conn = null;
        try {
            // 连接数据库
            conn = getConnection();
            // 设置事务隔离级别为提交读
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            // 开启事务, 开启事务即是关闭自动提交即可
            conn.setAutoCommit(false);
            // 更新用户信息
            update(conn, new User(users.get(0).id, "杨幂", 25));
            update(conn, new User(users.get(1).id, "李思思", 20));
            // 提交事务
            conn.commit();
            // 事务执行成功
            System.out.println("事务执行成功");
        } catch (SQLException e) {
            // 回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                System.out.println("回滚事务失败");
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            // 尝试关闭数据库连接
            try {
                // 恢复数据库自动提交记录
                conn.setAutoCommit(true);
                conn.close();
            } catch (SQLException e) {
                System.out.println("数据库关闭失败");
                e.printStackTrace();
            }
        }

        // 事务后重新打印所有用户列表
        System.out.println("事务后重新打印所有用户列表");
        try {
            users = getUsers();
            for (User user : users) {
                System.out.println(user);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        // 模拟事务回滚
        System.out.println("模拟事务回滚");
        try {
            // 连接数据库
            conn = getConnection();
            // 设置事务隔离级别为提交读
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            // 开启事务, 开启事务即是关闭自动提交即可
            conn.setAutoCommit(false);
            // 更新用户信息
            update(conn, new User(users.get(0).id, "周迅", 28));
            update(conn, new User(users.get(1).id, "卓依婷", 24));
            // 提交事务
            conn.commit();
            // 人为制造异常终止事务使其回滚, 不过后续代码也无法正常执行了
            throw new RuntimeException("强制数据库回滚");
        } catch (SQLException e) {
            // 回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                System.out.println("回滚事务失败");
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            // 尝试关闭数据库连接
            try {
                // 恢复数据库自动提交记录
                conn.setAutoCommit(true);
                conn.close();
            } catch (SQLException e) {
                System.out.println("数据库关闭失败");
                e.printStackTrace();
            }
        }
    }

    /**
     * 数据库连接
     * 
     * @return
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
    }

    /**
     * 获取用户列表
     * 
     * @return
     * @throws SQLException
     */
    private static List<User> getUsers() throws SQLException {
        try (Connection conn = getConnection()) {
            try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM user")) {
                try (ResultSet rs = ps.executeQuery()) {
                    List<User> users = new ArrayList<>();
                    while (rs.next()) {
                        long id = rs.getLong("id");
                        String name = rs.getString("name");
                        int age = rs.getInt("age");
                        users.add(new User(id, name, age));
                    }
                    return users;
                }
            }
        }
    }

    /**
     * 根据id更新用户姓名
     * 
     * @param conn
     * @param user
     * @throws SQLException
     */
    private static void update(Connection conn, User user) throws SQLException {
        try (PreparedStatement ps = conn.prepareStatement("UPDATE user SET name = ? WHERE id = ?")) {
            ps.setObject(1, user.name);
            ps.setObject(2, user.id);
            ps.executeUpdate();
        }
    }

}

Maven配置连接池组件:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>2.7.1</version>
</dependency>

常用JDBC连接池的实现:

  1. HikariCP
  2. C3P0
  3. BoneCP
  4. Druid

以连接池的方式来说明:

package github.banana;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 数据库连接池
 */
public class DBSource {
    // 配置MySQL连接属性
    private static final String JDBC_URL = "jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=utf8&serverTimezone=UTC";
    private static final String JDBC_USER = "root";
    private static final String JDBC_PASSWORD = "";

    /**
     * 使用连接池查询数据
     *
     * @param args
     *            输入参数
     */
    public static void main(String[] args) {
        // 创建连接池
        DataSource dataSource = create();
        // 保持线程实例
        List<Thread> threads = new ArrayList<>();
        // 启动4个线程来查询用户列表
        for (int i = 1; i <= 4; i++) {
            // 准备线程池
            Thread thread = new Thread() {
                public void run() {
                    // 暂缓一段时间
                    try {
                        Thread.sleep((long) (Math.random() * 1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        List<User> users = getUsers(dataSource);
                        for (User user : users) {
                            System.out.println(user);
                        }
                    } catch (SQLException e) {
                        e.getStackTrace();
                    }
                }
            };
            threads.add(thread);
        }
        // 启动线程
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.getStackTrace();
            }
        }
        // 关闭连接池
        ((HikariDataSource) dataSource).close();
    }

    /**
     * 配置数据库连接池
     *
     * @return DataSource
     */
    private static DataSource create() {
        // HikariCP是JDBC连接池组件
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(JDBC_URL);
        config.setUsername(JDBC_USER);
        config.setPassword(JDBC_PASSWORD);
        // 连接超时
        config.addDataSourceProperty("connectionTimeout", 1000);
        // 空闲超时
        config.addDataSourceProperty("idleTimeout", 1000);
        // 最大连接数
        config.addDataSourceProperty("maximumPoolSize", 10);

        return new HikariDataSource(config);
    }

    /**
     * 通过连接池获取用户列表
     *
     * @param dataSource
     *            数据库连接池
     * @return List<User>
     * @throws SQLException
     *             异常处理
     */
    private static List<User> getUsers(DataSource dataSource) throws SQLException {
        try (Connection conn = dataSource.getConnection()) {
            System.out.println("Using connection: " + conn);
            try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM user")) {
                try (ResultSet rs = ps.executeQuery()) {
                    return handleUsers(rs);
                }
            }
        }
    }

    /**
     * 获取用户列表
     *
     * @param rs
     *            ResultSet 结果集
     * @return List<User>
     * @throws SQLException
     *             异常处理
     */
    private static List<User> handleUsers(ResultSet rs) throws SQLException {
        List<User> users = new ArrayList<>();
        while (rs.next()) {
            long id = rs.getLong("id");
            String name = rs.getString("name");
            int age = rs.getInt("age");
            users.add(new User(id, name, age));
        }
        return users;
    }
}

六、网络

计算机网络的基本概念:

  1. 互联网:网络的网络;
  2. IP地址:计算机在网络中的标识;
  3. 网关:负责连接多个网络;
  4. 协议:TCP/IP协议;
  5. IP协议:分组交换协议;
  6. TCP协议:面向连接,可靠传输;
  7. UDP协议:不面向连接,不可靠传输。

1. TCP

  1. 客户端使用Socket(InetAddress, port)打开Socket;
  2. 服务器端用ServerSocket监听端口;
  3. 服务器端用accept接收连接并返回Socket;
  4. 双方通过Socket打开InputStream / OutputStream读写数据;
  5. flush()用于强制输出缓冲区。

客户端:

package github.banana;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * TCP客户端
 * 
 * @author zhgxun
 *
 */
public class TCPClient {

    /**
     * 向服务端请求一个日期
     * 
     * @param args
     * @throws IOException
     * @throws InterruptedException
     */
    public static void main(String[] args) throws IOException, InterruptedException {
        // 获取本机回环地址127.0.0.1
        InetAddress address = Inet4Address.getLoopbackAddress();
        // System.out.println(address);
        // 启动Socket连接
        try (Socket socket = new Socket(address, 9000)) {
            // 将数据读取到缓冲中
            try (BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
                // 将数据写入输出
                try (BufferedWriter bufferedWriter = new BufferedWriter(
                        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
                    // 向服务端写入信息
                    bufferedWriter.write("time\n");
                    bufferedWriter.flush();
                    // 从服务端读取响应
                    String resp = bufferedReader.readLine();
                    System.out.println("response time: " + resp);
                    // 停留1秒后继续发送请求
                    Thread.sleep(1000);
                    // 向服务端写入一个退出标识
                    bufferedWriter.write("q\n");
                    bufferedWriter.flush();
                    // 尝试从服务端读取信息
                    resp = bufferedReader.readLine();
                    System.out.println("response end: " + resp);
                }
            }
        }
    }

}

服务端:

package github.banana;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

/**
 * TCP服务端
 * 
 * @author zhgxun
 *
 */
public class TCPServer {

    /**
     * 服务只能运行一次
     * 
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // 启动给一个Socket实例
        try (ServerSocket serverSocket = new ServerSocket(9000)) {
            System.out.println("TCP server Socket start...");
            // 接收请求
            try (Socket socket = serverSocket.accept()) {
                // 读取客服端请求
                try (BufferedReader bufferedReader = new BufferedReader(
                        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
                    // 写会数据到客户端
                    try (BufferedWriter bufferedWriter = new BufferedWriter(
                            new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
                        // 读取一行
                        String line = bufferedReader.readLine();
                        // 如果读到客户端约定的字段则返回时间
                        if ("time".equals(line)) {
                            bufferedWriter.write(LocalDateTime.now().toString());
                            bufferedWriter.flush();
                        } else {
                            bufferedWriter.write("None, please request 'time'");
                            bufferedWriter.flush();
                        }
                    }
                }
            }
        }
        System.out.println("Socket done");
    }

}

多线程运行:

package github.banana;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 多线程
 * 
 * @author zhgxun
 *
 */
public class MultiTcpServer {

    /**
     * 多线程服务端
     * 
     * @param args
     */
    public static void main(String[] args) {
        try {
            // 启动Socket监听9000端口
            @SuppressWarnings("resource")
            ServerSocket serverSocket = new ServerSocket(9000);
            System.out.println("Socket start...");
            // 服务器端使用无限循环监听客服端请求
            for (;;) {
                // 每次accept()返回后,创建新的线程来处理客户端请求
                Socket socket = serverSocket.accept();
                System.out.println("Accepting from to: " + socket.getRemoteSocketAddress());
                // 使用多线程处理类处理, 每个客户端请求对应一个服务线程
                TCPHandler handler = new TCPHandler(socket);
                // 启动多线程
                handler.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

TCPHandler 需要实现Thread接口的run()方法:

package github.banana;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

/**
 * 多线程处理
 * 
 * @author zhgxun
 *
 */
public class TCPHandler extends Thread {
    Socket socket;

    /**
     * 构造化
     * 
     * @param socket
     */
    public TCPHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        // 读缓冲
        try (BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(this.socket.getInputStream(), StandardCharsets.UTF_8))) {
            // 写缓冲
            try (BufferedWriter bufferedWriter = new BufferedWriter(
                    new OutputStreamWriter(this.socket.getOutputStream(), StandardCharsets.UTF_8))) {
                // 循环读写每个请求中的请求数据
                for (;;) {
                    String line = bufferedReader.readLine();
                    // 如果需要退出
                    if ("q".equals(line)) {
                        bufferedWriter.write("Good bye!\n");
                        bufferedWriter.flush();
                        break;
                    } else if ("time".equals(line)) {
                        bufferedWriter.write(LocalDateTime.now().toString() + "\n");
                        bufferedWriter.flush();
                    } else {
                        bufferedWriter.write("None!");
                        bufferedWriter.flush();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 不管是否处理异常, 都尝试关闭连接
            try {
                this.socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

2. SMTP

SMTP: Simple Mail Transport Protocol。

标准端口25,加密端口465/587。

使用javamail发送Email:

  1. 确定SMTP服务器信息:域名/端口/使用明文/SSL/TLS;
  2. 调用相关API发送Email;
  3. 设置debug模式可以查看通信详细内容。

邮件组件:

<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4</version>
</dependency>

SMTP是一个应答模型:

DEBUG: setDebug: JavaMail version 1.4ea
DEBUG: getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Sun Microsystems, Inc]
DEBUG SMTP: useEhlo true, useAuth true
DEBUG SMTP: useEhlo true, useAuth true
DEBUG SMTP: trying to connect to host "smtp.sina.com", port 465, isSSL false
220 smtp-2-32.smtpsmail.fmail.yf.sinanode.com ESMTP
DEBUG SMTP: connected to host "smtp.sina.com", port: 465

EHLO zhgxun-pro
250-smtp-2-32.smtpsmail.fmail.yf.sinanode.com
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN PLAIN
250-STARTTLS
250 8BITMIME
DEBUG SMTP: Found extension "AUTH", arg "LOGIN PLAIN"
DEBUG SMTP: Found extension "AUTH=LOGIN", arg "PLAIN"
DEBUG SMTP: Found extension "STARTTLS", arg ""
DEBUG SMTP: Found extension "8BITMIME", arg ""
DEBUG SMTP: Attempt to authenticate
AUTH LOGIN
334 VXNlcm5hbWU6
emhneHVuMTk4OUBzaW5hLmNvbQ==
334 UGFzc3dvcmQ6
YnIxc2om
235 OK Authenticated
DEBUG SMTP: use8bit false
MAIL FROM:<zhgxun1989@sina.com>
250 ok
RCPT TO:<zhgxun1989@163.com>
250 ok
DEBUG SMTP: Verified Addresses
DEBUG SMTP:   zhgxun1989@163.com
DATA
354 End data with <CR><LF>.<CR><LF>
From: zhgxun1989@sina.com
To: zhgxun1989@163.com
Message-ID: <455896770.01514724480880.JavaMail.zhgxun@zhgxun-pro>
Subject: =?UTF-8?B?5Y2D5LiO5Y2D5a+7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64

5aSp56m65piv6L+e552A55qE77yM5aaC5p6c5oiR5Lus5Lmf6IO95ZCE6Ieq5Y+R5YWJ55qE6K+d
77yM5peg6K666Led56a75pyJ5aSa6L+c77yM6YO96IO955yL5Yiw5b285q2k5Yqq5Yqb55qE6Lqr
5b2x44CC
.
250 ok queue id 552255407018
QUIT
221 smtp-2-32.smtpsmail.fmail.yf.sinanode.com

下面是用sina的邮箱进行的测试,每个邮件服务商都有自己的使用方式,需要根据邮件进行调试才行。

package github.banana;

import java.io.IOException;
import java.io.InputStream;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;

/**
 * 发送邮件
 *
 * @author zhgxun
 */
public class SendMail {
    public static void main(String[] args) {
        text();
        html();
        try {
            attachment();
            image();
        } catch (IOException | MessagingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送文本邮件
     */
    private static void text() {
        Email email = new Email();
        Session session = email.SSLSession();
        try {
            Message message = email.text(session, "zhgxun1989@sina.com", "zhgxun1989@163.com", "千与千寻",
                    "天空是连着的,如果我们也能各自发光的话,无论距离有多远,都能看到彼此努力的身影。");
            Transport.send(message);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送html格式邮件
     */
    private static void html() {
        Email email = new Email();
        Session session = email.SSLSession();
        Message message;
        try {
            message = email.text(session, "zhgxun1989@sina.com", "zhgxun1989@163.com", "风晴雪独白",
                    "<p>曾经有人告诉过我,对生死之事毫无执念的人,只是因为还没有经历过真正绝望的别离。仿佛诅咒一般……</p>");
            Transport.send(message);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送图片附件
     *
     * @throws IOException
     *             IO异常
     * @throws MessagingException
     *             Message异常
     */
    private static void attachment() throws IOException, MessagingException {
        Email email = new Email();
        Session session = email.SSLSession();
        try (InputStream inputStream = email.getClass().getResourceAsStream("/LinuxStory.jpeg")) {
            Message message = email.attachment(session, "zhgxun1989@sina.com", "zhgxun1989@163.com", "这是一次北京的小聚会",
                    "还记得15年华华去北京的时候,我们 LinuxStory 群里的几个“死党”聚会并没有正式的分享。我们晚上撸串,撸了串去中关村创业大街晃了一圈,谈天说地。10月北京的深夜还是透心凉的,但是我们几个好像怎么都聊不完,烤串吃了 n 久+逛了创业大街后说应该回家了,但是走到一个十字路口还是不自觉地停下来又聊了很久很久,路上的行人越来越少……那时候的场景还深深印在我脑海!",
                    "LinuxStory.jpeg", inputStream);
            Transport.send(message);
        }
    }

    /**
     * 发送内嵌图片
     *
     * @throws IOException
     *             IO异常
     * @throws MessagingException
     *             Message异常
     */
    private static void image() throws IOException, MessagingException {
        Email email = new Email();
        Session session = email.SSLSession();
        try (InputStream inputStream = email.getClass().getResourceAsStream("/LinuxStory.jpeg")) {
            Message message = email.attachment(session, "zhgxun1989@sina.com", "zhgxun1989@163.com", "这是一次北京的小聚会",
                    "还记得15年华华去北京的时候,我们 LinuxStory 群里的几个“死党”聚会并没有正式的分享。我们晚上撸串,撸了串去中关村创业大街晃了一圈,谈天说地。10月北京的深夜还是透心凉的,但是我们几个好像怎么都聊不完,烤串吃了 n 久+逛了创业大街后说应该回家了,但是走到一个十字路口还是不自觉地停下来又聊了很久很久,路上的行人越来越少……那时候的场景还深深印在我脑海!<img src=\"cid:img01\">LinuxStory 的线下聚会就是这样,不管在哪里,你都会觉得暖心,你都会感受到真诚,你都会爱上我们的气氛。",
                    "LinuxStory.jpeg", inputStream);
            Transport.send(message);
        }
    }
}

可以发送文本,最主要的要发送附件。发送邮件的功能运用较为普遍,但是收邮件基本都是客户端实现,无需赘述。

package github.banana;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;

/**
 * 邮箱公共信息
 *
 * @author zhgxun
 */
public class Email {
    // 邮箱服务地址
    private String host = "smtp.sina.com";
    // 邮箱用户名
    private String username = "zhgxun1989@sina.com";
    // 邮箱密码
    private String password = "br1sj&";
    // 是否开启调试模式
    private boolean debug = true;
    // 邮箱服务端口
    private String port = "465";

    public Email() {

    }

    /**
     * 可复写默认配置
     *
     * @param host     邮件主机
     * @param username 用户名
     * @param password 密码
     * @param debug    调试模式
     * @param port     端口
     */
    public Email(String host, String username, String password, boolean debug, String port) {
        this.host = host;
        this.username = username;
        this.password = password;
        this.debug = debug;
        this.port = port;
    }

    /**
     * 使用SSL方式认证
     *
     * @return javax.mail.Session
     */
    public Session SSLSession() {
        Properties properties = new Properties();
        // 主机
        properties.put("mail.smtp.host", this.host);
        // 端口
        properties.put("mail.smtp.port", this.port);
        // 启用用户登陆认证
        properties.put("mail.smtp.auth", "true");
        // 启用SSL
        properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        properties.put("mail.smtp.socketFactory.port", this.port);
        // 用户认证
        Session session = Session.getInstance(properties, new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                Email email = new Email();
                return new PasswordAuthentication(email.username, email.password);
            }
        });
        // 显示调试模式
        session.setDebug(this.debug);
        return session;
    }

    /**
     * 发送文本
     *
     * @param session javax.mail.Session
     * @param from    发信人
     * @param to      收信人
     * @param subject 邮件主题
     * @param body    邮件内容
     * @return Message
     * @throws MessagingException Message异常
     */
    public Message text(Session session, String from, String to, String subject, String body)
            throws MessagingException {
        // 使用MimeMessage来发送邮件
        MimeMessage mimeMessage = new MimeMessage(session);
        // 设置发件人
        mimeMessage.setFrom(new InternetAddress(from));
        // 设置收件人
        mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        // 设置主题
        mimeMessage.setSubject(subject, "UTF-8");
        // 设置内容
        mimeMessage.setText(body, "UTF-8");
        return mimeMessage;
    }

    /**
     * 发送html格式邮件
     *
     * @param session
     * @param from
     * @param to
     * @param subject
     * @param body
     * @return
     * @throws MessagingException
     */
    public Message html(Session session, String from, String to, String subject, String body)
            throws MessagingException {
        // 使用MimeMessage来发送邮件
        MimeMessage mimeMessage = new MimeMessage(session);
        // 设置发件人
        mimeMessage.setFrom(new InternetAddress(from));
        // 设置收件人
        mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        // 设置主题
        mimeMessage.setSubject(subject, "UTF-8");
        // 设置内容
        mimeMessage.setText(body, "UTF-8", "html");
        return mimeMessage;
    }

    /**
     * 发送图片附件
     *
     * @param session
     * @param from
     * @param to
     * @param subject
     * @param body
     * @param fileName
     * @param inputStream
     * @return
     * @throws MessagingException
     * @throws IOException
     */
    public Message attachment(Session session, String from, String to, String subject, String body, String fileName,
                              InputStream inputStream) throws MessagingException, IOException {
        // 使用MimeMessage来发送邮件
        MimeMessage mimeMessage = new MimeMessage(session);
        // 设置发件人
        mimeMessage.setFrom(new InternetAddress(from));
        // 设置收件人
        mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        // 设置主题
        mimeMessage.setSubject(subject, "UTF-8");
        // 设置邮件主体body
        BodyPart bodyPart = new MimeBodyPart();
        bodyPart.setContent(body, "text/html;charset=utf-8");
        // 发送附件需要使用multi
        Multipart multipart = new MimeMultipart();
        // 将主体添加到multi中
        multipart.addBodyPart(bodyPart);
        // 添加一张图片附件
        BodyPart imagePart = new MimeBodyPart();
        imagePart.setFileName(fileName);
        imagePart.setDataHandler(new DataHandler(new ByteArrayDataSource(inputStream, "application/octet-stream")));
        multipart.addBodyPart(imagePart);
        mimeMessage.setContent(multipart);
        return mimeMessage;
    }

    /**
     * 发送一张内嵌的图片
     *
     * @param session
     * @param from
     * @param to
     * @param subject
     * @param body
     * @param fileName
     * @param inputStream
     * @return
     * @throws MessagingException
     * @throws IOException
     */
    public Message image(Session session, String from, String to, String subject, String body, String fileName,
                         InputStream inputStream) throws MessagingException, IOException {
        // 使用MimeMessage来发送邮件
        MimeMessage mimeMessage = new MimeMessage(session);
        // 设置发件人
        mimeMessage.setFrom(new InternetAddress(from));
        // 设置收件人
        mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        // 设置主题
        mimeMessage.setSubject(subject, "UTF-8");
        // 设置邮件主体body
        BodyPart bodyPart = new MimeBodyPart();
        bodyPart.setContent(body, "text/html;charset=utf-8");
        // 发送附件需要使用multi
        Multipart multipart = new MimeMultipart();
        // 将主体添加到multi中
        multipart.addBodyPart(bodyPart);
        // 添加一张图片附件
        BodyPart imagePart = new MimeBodyPart();
        imagePart.setFileName(fileName);
        imagePart.setDataHandler(new DataHandler(new ByteArrayDataSource(inputStream, "application/octet-stream")));
        // 与HTML的<img src="cid:img01">关联:
        imagePart.setHeader("Content-ID", "<img01>");
        multipart.addBodyPart(imagePart);
        mimeMessage.setContent(multipart);
        return mimeMessage;
    }

}

3. HTTP

HTTP: HyperText Transfer Protocol:

  1. HTTP协议是一个基于TCP的请求/响应协议;
  2. 请求和响应的结构分为header和body两部分;
  3. header是必须的;
  4. body可选;
  5. 广泛用于浏览器、手机App与服务器的数据交互。

java.net.Java提供了HttpURLConnection实现HTTP客户端。

格式化输入内容:

package github.banana;

import java.nio.charset.StandardCharsets;

/**
 * 将响应内容格式化输出
 * 
 * @author zhgxun
 *
 */
public class Response {
    private int code;
    private byte[] data;

    public Response(int code, byte[] data) {
        this.code = code;
        this.data = data;
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder(2014);
        // 响应码
        stringBuilder.append(this.code).append("\n");
        // 把内容转化为字符串
        String string = new String(data, StandardCharsets.UTF_8);
        // 如果长度超过1024则拼接为...展示
        if (string.length() > 1024) {
            stringBuilder.append(string.substring(0,  1024)).append("\n...");
        } else {
            stringBuilder.append(string);
        }
        return stringBuilder.toString();
    }
}

使用GET,POST发送数据到服务端:

package github.banana;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Http编程
 * 
 * @author zhgxun
 *
 */
public class HttpClient {

    public static void main(String[] args) {
        // get
        System.out.println("start get request...");
        Response response = get("https://www.douban.com");
        System.out.println(response);

        // post
        System.out.println("start post send...");
        Map<String, String> map = new HashMap<>();
        map.put("form_email", "zhgxun");
        map.put("form_password", "cqzytsy%");

        // 初始化一个列表
        List<String> list = new ArrayList<>(map.size());
        for (String string : map.keySet()) {
            try {
                list.add(string + "=" + URLEncoder.encode(map.get(string), "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        // 将参数拼接成地址格式
        String query = String.join("&", list);

        try {
            Response resp = post("https://www.douban.com/accounts/login", "application/x-www-form-urlencoded", query);
            System.out.println(resp);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * get方式获取数据
     * 
     * @param theUrl
     * @return
     */
    private static Response get(String theUrl) {
        HttpURLConnection httpURLConnection = null;
        try {
            // 将请求地址转化为一个URL对象
            URL url = new URL(theUrl);
            // 将URL对象强制转化为HttpURLConnection连接
            httpURLConnection = (HttpURLConnection) url.openConnection();
            // 初始化一个输出流
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            // 从输入流中读取内容
            try (InputStream inputStream = httpURLConnection.getInputStream()) {
                // 以byte方式一次读取1024字节
                byte[] bytes = new byte[1024];
                int n;
                while ((n = inputStream.read(bytes)) >= 0) {
                    // 将读取到的内容写入到输出存储中
                    byteArrayOutputStream.write(bytes, 0, n);
                }
                // 将读取到的内容响应输出
                return new Response(httpURLConnection.getResponseCode(), byteArrayOutputStream.toByteArray());
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            httpURLConnection.disconnect();
        }
        return null;
    }

    /**
     * post方式发送数据
     * 
     * @param theUrl
     * @param type
     * @param data
     * @return
     * @throws IOException
     */
    private static Response post(String theUrl, String type, String data) throws IOException {
        HttpURLConnection httpURLConnection = null;
        try {
            URL url = new URL(theUrl);
            httpURLConnection = (HttpURLConnection) url.openConnection();

            // 设置请求方式为POST
            httpURLConnection.setRequestMethod("POST");
            // 设置开始输入请求内容
            httpURLConnection.setDoOutput(true);
            // 将请求体转化为字节数组
            byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
            // 设置内容类型
            httpURLConnection.setRequestProperty("Content-Type", type);
            // 设置内容长度为字符串
            httpURLConnection.setRequestProperty("Content-Length", String.valueOf(bytes.length));

            // 获取连接内容输出对象
            OutputStream outputStream = httpURLConnection.getOutputStream();
            // 将内容写入到输出对象中
            outputStream.write(bytes);

            // 保存响应存储对象
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            // 从请求对象中获取输入对象
            InputStream inputStream = httpURLConnection.getInputStream();
            // 一次性读取1024个字节
            byte[] b = new byte[1024];
            int n;
            while ((n = inputStream.read(b)) >= 0) {
                // 将内容存储到输出对象中
                byteArrayOutputStream.write(b, 0, n);
            }

            // 返回响应的内容
            return new Response(httpURLConnection.getResponseCode(), byteArrayOutputStream.toByteArray());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ProtocolException e) {
            e.printStackTrace();
        } finally {
            httpURLConnection.disconnect();
        }
        return null;
    }
}

七、多线程

1. 进程和线程

进程和线程的关系:

  1. 一个进程可以包含一个或多个线程,至少一个线程;
  2. 线程是操作系统调度的最小任务单位;
  3. 如何调度线程完全由操作系统决定。

实现多任务的方法:

  1. 多进程模式,一个进程只有一个线程;
  2. 多线程模式,一个进程内有多个线程;
  3. 多进程+多线程模式,复杂度最高。

多进程和多线程:

  1. 创建进程比创建线程开销大;
  2. 进程间通信比线程间通信慢;
  3. 多进程稳定性比多线程高,进程间互相独立,但是任何一个线程的崩溃都会导致进程奔溃。

Java内置多线程支持:

  1. 一个Javac程序实际上是一个JVM进程;
  2. JVM用一个主线程来执行main()方法;
  3. 在main()方法中又可以启动多个线程。

多线程编程的特点:

  1. 多线程需要读写共享数据;
  2. 多线程经常需要同步;
  3. 多线程编程的复杂度高,调试更困哪。

Java多线程编程的特点:

  1. 多线程模型是Java程序最基本的并发模型;
  2. 网络、数据库、Web等都依赖多线程模型;
  3. 必须掌握Java多线程编程才能继续深入学习。

2. 创建新线程

  1. 从Thread派生,通常由该类派生,覆写run()方法;
  2. 实现Runnable接口,对于无法从Thread类派生的类,可以实现该接口。

Java用Thread类表示一个线程,通常调用start()启动一个线程。

一个线程对象只能调用一次start(),查看源码可以知道,start()继续调用start0():

private native void start0();

native表示线程启动是用JVM虚拟机来操作的,由C代码实现,并不是Java实现的。

线程的执行代码是run()方法,但不能直接调用run()方法,这样run()就成为一个普通的Java方法,而不是启动给一个线程。

线程调度由操作系统决定,程序无法决定。

Thread.sleep(1000)可以把当前线程暂停一段时间,单位毫秒。

3. 线程的状态

Java线程对象Thread的状态包括:

  1. New 新创建;
  2. Runnable 运行中;
  3. Blocked 被阻塞;
  4. Waiting 等待;
  5. Timed Waiting 计时等待;
  6. Terminated 已终止。

线程终止的原因:

  1. run()方法执行到语句返回,线程正常终止;
  2. 因为未捕获的异常导致线程意外终止;
  3. 对某个线程的Thread实例调用stop()方法强制终止,不推荐。

通过对另一个线程对象调用join()方法可以等待其执行结束。

可以指定等待时间,超过等待时间线程仍然没有结束就不再等待。

对已经运行结束的线程调用join()方法会立刻返回。

4. 中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程,中断线程就是其它线程给该线程发一个信号,该线程收到信号后结束run()方法。

中断线程需要通过检测isInterrupted()标志,其它线程通过调用interrupt()方法可以中断一个线程。

如果线程处于等待状态,该线程会捕获InterruptedException,捕获到InterruptedException说明有其它线程对其调用了interrupt()方法,通常情况下该线程会立即结束运行。

isInterrupted()为true或者捕获了InterruptedException都应该立刻结束。

还可以通过设置running标志位,线程间共享变量需要使用volatile关键字标记,确保线程能读取到更新后变量的值。

Java内存模型:

虚拟机中有一块主内存存储线程间的共享变量,线程也有自己的私有变量。通常线程间修改了变量的值后,会立即更新到主内存中。

volatile关键字解决了共享变量在线程间的可见性问题,该关键字的目的是告诉虚拟机:

  1. 每次访问变量时,总是获取主内存的最新值;
  2. 每次修改变量后,立即会写到主内存。

5. 守护线程

有一种线程的目的就是无限循环, 比如定时任务。

如果某个线程不结束, JVM线程就无法结束。

守护线程负责结束这类线程。

守护线程是为其它线程服务的线程。

所有非守护线程都结束完毕后, 虚拟机退出。

守护线程的特点是不能持有资源, 因为虚拟机退出后, 无法再释放资源等操作。

创建线程对象后立即调用setDaemon(true)即可创建守护线程。

6. 线程同步

多线程同时修改变量,会造成逻辑错误。

对共享变量进行操作时,必须保证是原子操作,原子操作是指不能被中断的一个或一系列操作。

必须保证一系列操作执行过程中不被其他线程执行,必须进行加锁和解锁。

Java使用synchronized对一个对象进行加锁,保证了代码块在任意时刻最多只有一个线程能执行。

造成的问题性能下降,加锁解锁都消耗时间。

如何使用synchronized:

  1. 找出修改共享变量的线程代码块;
  2. 选择一个实例作为锁;
  3. 使用synchronized(lockObject) {…}。

同步的本质就是给指定对象加锁,注意加锁对象必须是同一个实例,对JVM定义的单个原子操作不需要同步。

用synchronized修饰方法可以把整个方法变为同步代码块;

synchronized方法加锁对象是this;

通过合理的设计和数据封装可以让一个类变为“线程安全”;

一个类没有特殊说明,默认不是thread-safe;

多线程能否访问某个非线程安全的实例,需要具体问题具体分析。

死锁产生的条件:多线程各自持有不同的锁,并互相试图获取对方已持有的锁,双方无限等待下去:导致死锁。

如何避免死锁:多线程获取锁的顺序要一致。

死锁发生以后,没有任何机制能解除死锁,只能强制结束JVM进程。

wait / notify多线程协调。

当条件不满足时线程进入等待状态。

在synchronized内部可以调用wait()使线程进入等待状态。

必须在已获得的锁对象上调用wait()方法。

在synchronized内部可以调用notify()/notifyAll()唤醒其他等待线程。

必须在已获得的锁对象上调用notify()/notifyAll()方法。

package com.feiyangedu.sample;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 任务队列
 * 
 * @author zhgxun
 *
 */
class TaskQueue {

    final Queue<String> queue = new LinkedList<>();

    /**
     * 添加任务
     * 
     * @return
     * @throws InterruptedException
     */
    public synchronized String getTask() throws InterruptedException {
        // 如果队列为空, 会使线程进入等待状态,释放this锁,wait()方法是JVM虚拟机中的C代码实现的
        // 并且该方法只能在synchronized中使用
        while (this.queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }

    /**
     * 获取一个任务
     * 
     * @param name
     */
    public synchronized void addTask(String name) {
        this.queue.add(name);
        // 一旦该任务被添加激活, 必须通知所有的处理线程
        this.notifyAll();
    }
}

/**
 * Worker线程
 * 
 * @author zhgxun
 *
 */
class WorkerThread extends Thread {
    TaskQueue taskQueue;

    public WorkerThread(TaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void run() {
        // 线程未中断就一直运行
        while (!isInterrupted()) {
            String name;
            try {
                name = taskQueue.getTask();
            } catch (InterruptedException e) {
                break;
            }
            String result = "Hello, " + name + "!";
            System.out.println(result);
        }
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        TaskQueue taskQueue = new TaskQueue();
        WorkerThread worker = new WorkerThread(taskQueue);
        worker.start();
        // add task:
        taskQueue.addTask("Bob");
        Thread.sleep(1000);
        taskQueue.addTask("Alice");
        Thread.sleep(1000);
        taskQueue.addTask("Tim");
        Thread.sleep(1000);
        worker.interrupt();
        worker.join();
        System.out.println("END");
    }
}

7. 高级concurrent包

1. ReentrantLock

ReentrantLock可以替代synchronized。

ReentrantLock获取锁更安全。

必须使用try … finally保证正确获取和释放锁。

tryLock()可指定超时。

package com.feiyangedu.sample;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {

    private Lock lock = new ReentrantLock();

    private int value = 0;

    public void add(int m) {
        lock.lock();
        try {
            this.value += m;
        } finally {
            lock.unlock();
        }
    }

    public void dec(int m) {
        lock.lock();
        try {
            this.value -= m;
        } finally {
            lock.unlock();
        }
    }

    public int get() {
        lock.lock();
        try {
            return this.value;
        } finally {
            lock.unlock();
        }
    }
}

public class Main {

    final static int LOOP = 100;

    public static void main(String[] args) throws Exception {
        Counter counter = new Counter();
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < LOOP; i++) {
                    counter.add(1);
                }
            }
        };
        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < LOOP; i++) {
                    counter.dec(1);
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

2. ReadWriteLock

ReentrantLock配合使用ReadWriteLock可以提高读取效率:

  1. ReadWriteLock只允许一个线程写入;
  2. ReadWriteLock允许多个线程同时读取;
  3. ReadWriteLock适合读多写少的场景。
package com.feiyangedu.sample;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Counter {

    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock rlock = lock.readLock();
    private Lock wlock = lock.writeLock();

    private int value = 0;

    public void add(int m) {
        wlock.lock();
        try {
            this.value += m;
        } finally {
            wlock.unlock();
        }
    }

    public void dec(int m) {
        wlock.lock();
        try {
            this.value -= m;
        } finally {
            wlock.unlock();
        }
    }

    public int get() {
        rlock.lock();
        try {
            return this.value;
        } finally {
            rlock.unlock();
        }
    }
}

public class Main {

    final static int LOOP = 100;

    public static void main(String[] args) throws Exception {
        Counter counter = new Counter();
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < LOOP; i++) {
                    counter.add(1);
                }
            }
        };
        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < LOOP; i++) {
                    counter.dec(1);
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

3. Condition

Condition可以替代wait / notify。

Condition对象必须从ReentrantLock对象获取。

ReentrantLock+Condition可以替代synchronized + wait / notify。

package com.feiyangedu.sample;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class TaskQueue {

    final Queue<String> queue = new LinkedList<>();

    final Lock lock = new ReentrantLock();
    final Condition notEmpty = lock.newCondition();

    public String getTask() throws InterruptedException {
        lock.lock();
        try {
            while (this.queue.isEmpty()) {
                notEmpty.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }

    public void addTask(String name) {
        lock.lock();
        try {
            this.queue.add(name);
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

class WorkerThread extends Thread {
    TaskQueue taskQueue;

    public WorkerThread(TaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void run() {
        while (!isInterrupted()) {
            String name;
            try {
                name = taskQueue.getTask();
            } catch (InterruptedException e) {
                break;
            }
            String result = "Hello, " + name + "!";
            System.out.println(result);
        }
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        TaskQueue taskQueue = new TaskQueue();
        WorkerThread worker = new WorkerThread(taskQueue);
        worker.start();
        // add task:
        taskQueue.addTask("Bob");
        Thread.sleep(1000);
        taskQueue.addTask("Alice");
        Thread.sleep(1000);
        taskQueue.addTask("Tim");
        Thread.sleep(1000);
        worker.interrupt();
        worker.join();
        System.out.println("END");
    }
}

4. Concurrent集合

使用java.util.concurrent提供的Blocking集合可以简化多线程编程:

  1. CopyOnWriteArrayList;
  2. ConcurrentHashMap;
  3. CopyOnWriteArraySet;
  4. ArrayBlockingQueue;
  5. LinkedBlockingQueue;
  6. LinkedBlockingDeque。

多线程同时访问Blocking集合是安全的,尽量使用JDK提供的concurrent集合,避免自己编写同步代码。

package com.feiyangedu.sample;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class WorkerThread extends Thread {
    BlockingQueue<String> taskQueue;

    public WorkerThread(BlockingQueue<String> taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void run() {
        while (!isInterrupted()) {
            String name;
            try {
                name = taskQueue.take();
            } catch (InterruptedException e) {
                break;
            }
            String result = "Hello, " + name + "!";
            System.out.println(result);
        }
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        BlockingQueue<String> taskQueue = new ArrayBlockingQueue<>(100);
        WorkerThread worker = new WorkerThread(taskQueue);
        worker.start();
        // add task:
        taskQueue.put("Bob");
        Thread.sleep(1000);
        taskQueue.put("Alice");
        Thread.sleep(1000);
        taskQueue.put("Tim");
        Thread.sleep(1000);
        worker.interrupt();
        worker.join();
        System.out.println("END");
    }
}

5. Fork/Join

Fork/Join是一种基于“分治”的算法:分解任务+合并结果。

ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行。

任务类必须继承自RecursiveTask/RecursiveAction。

使用Fork/Join模式可以进行并行计算提高效率。

package com.feiyangedu.sample;

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

class SumTask extends RecursiveTask<Long> {
    private static final long serialVersionUID = 1L;
    static final int THRESHOLD = 300;
    long[] array;
    int start;
    int end;

    SumTask(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 如果任务足够小,直接计算:
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += this.array[i];
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    e.getStackTrace();
                }
            }
            return sum;
        }
        // 任务太大,一分为二:
        int middle = (end + start) / 2;
        System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
        SumTask subtask1 = new SumTask(this.array, start, middle);
        SumTask subtask2 = new SumTask(this.array, middle, end);
        invokeAll(subtask1, subtask2);
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        Long result = subresult1 + subresult2;
        System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
        return result;
    }

}

public class ForkJoinTaskSample {

    public static void main(String[] args) throws Exception {
        // 创建1000个随机数组成的数组:
        long[] array = new long[1000];
        long expectedSum = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = random();
            expectedSum += array[i];
        }
        System.out.println("Expected sum: " + expectedSum);
        // fork/join:
        ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
        long startTime = System.currentTimeMillis();
        Long result = ForkJoinPool.commonPool().invoke(task);
        long endTime = System.currentTimeMillis();
        System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
    }

    static Random random = new Random(0);

    static long random() {
        return random.nextInt(10000);
    }
}

通过修改发现,其实程序的运行效率是有一个范围的,超出了该范围,性能就不明显。

八、反射与泛型

1. class

class本身是一种数据类型(Type),class/interface的数据类型是Class,JVM为每个加载的class创建唯一的Class实例。

Class实例包含该class的所有信息,通过Class实例获取class信息的方法称为反射(Reflection)。

获取一个class的Class实例:

  1. Class cls = String.class;
  2. Class cls = “str”.getClass();
  3. Class cls = Class.forName(“java.lang.String”)。

注意Class的==比较和instanceof的区别。

从Class实例获取class信息:

  1. getName()
  2. getSimpleName()
  3. getPackage()

从Class实例判断class类型:

  1. isInterface()
  2. isEnum()
  3. isArray()
  4. isPrimitive()

创建class实例:cls.newInstance()。

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

2. Field

通过Class实例获取字段field信息:

  1. getField(name):获取某个public的field(包括父类)
  2. getDeclaredField(name):获取当前类的某个field(不包括父类)
  3. getFields():获取所有public的field(包括父类)
  4. getDeclaredFields():获取当前类的所有field(不包括父类)。

Field对象包含一个field的所有信息:

  1. getName()
  2. getType()
  3. getModifiers()

获取和设置field的值:

  1. get(Object obj)
  2. set(Object, Object)

通过反射访问Field需要通过SecurityManager设置的规则。

通过设置setAccessible(true)来访问非public字段。

3. Method

通过Class实例获取方法Method信息:

  1. getMethod(name, Class…):获取某个public的method(包括父类)
  2. getDeclaredMethod(name, Class…):获取当前类的某个method(不包括父类)
  3. getMethods():获取所有public的method(包括父类)
  4. getDeclaredMethods():获取当前类的所有method(不包括父类)

Method对象包含一个method的所有信息:

  1. getName()
  2. getReturnType()
  3. getParameterTypes()
  4. getModifiers()

调用Method:Object invoke(Object obj, Object… args)。

通过设置setAccessible(true)来访问非public方法。

反射调用Method也遵守多态的规则。

4. Constructor

调用public无参数构造方法:Class.newInstance()。

通过Class实例获取Constructor信息:

  1. getConstructor(Class…):获取某个public的Constructor
  2. getDeclaredConstructor(Class…):获取某个Constructor
  3. getConstructors():获取所有public的Constructor
  4. getDeclaredConstructors():获取所有Constructor

通过Constructor实例可以创建一个实例对象:newInstance(Object… parameters)。

通过设置setAccessible(true)来访问非public构造方法。

5. 注解

注解(Annotation)是放在Java源码的类、方法、字段、参数前的一种标签。

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

编译器可以使用的注解:

@Override,让编译器检查该方法是否正确实现了覆写;
@Deprecated,这个方法被标记为作废,会出现下划线;
@SuppressWarnings,如果出现警告,会忽略该警告。

注解可以定义配置参数和默认值。

注解可以定义配置参数:

  1. 配置参数由注解类型定义;
  2. 配置参数可以包括:所有基本类型,String,枚举类型,数组;
  3. 配置参数必须是常量。

检查注解:

@Check(min=0, max=100, value=55)
public int a;

赋值为99:

@Check(value=99)
public int a;

相当于@Check(value=99):

@Check(99)
public int a;

无参数则全部使用默认值:

@Check
public int a;

6. 定义注解

使用@interface定义注解(Annotation)。

使用元注解定义注解:

  1. @Target,定义注解可以用于源码的哪些位置:ElementType.TYPE-类和接口,FIELD-字段,METHOD-方法,PARAMETER-参数,CONSTRUCTOR-构造方法;
  2. @Retention,定义注解的生命周期,RetentionPolicy.SOURCE-编译期,编译器在编译时直接丢弃,比如覆写,CLASS-class文件,仅存储在class文件,RUNTIME-运行,运行期可以读取该注解,默认是CLASS,通常是RUNTIME;
  3. @Repeatable,是否可重复
  4. @Inherited,子类是否可以继承父类定义的注解,仅针对Target.TYPE类型的注解,仅针对class,对interface的继承无效。

定义Annotation的步骤:

  1. 用@interface定义注解
  2. 用元注解(meta annotation)配置注解,Target:必须设置,Retention:一般设置为RUNTIME;
  3. 通常不必写@Inherited, @Repeatable等等
  4. 定义注解参数和默认值

7. 泛型

泛型(Generic)就是定义一种模板,例如ArrayList

在代码中为用到的类创建对应的ArrayList<类型>:ArrayList strList = new ArrayList()。

编译器会针对泛型类型作检查。 要注意泛型的继承关系。