Java基础知识
此文仅为笔记,为自己学习参考使用。
此文大部分内容均来自廖雪峰老师的博客,十分感谢他的教程。
原文地址在:https://www.liaoxuefeng.com/wiki/1252599548343744
Java 13 Overview
基本知识
版本区别
Java EE > Java SE > Java ME
Java SE 就是标准的JVM和标准库,Java EE是企业版,只是在Java SE的基础上方便开发Web应用、数据库、消息服务等,Java EE使用的虚拟机与Java SE完全相同,Java ME针对嵌入式设备。
JDK与JRE
- JDK: Java Development Kit,包含JRE和编译器、调试器等开发工具。
- JRE: Java Runtime Environment,含JVM和Runtime Library。
设置环境变量
JAVA_HOME:指向JDK安装目录。
C:\Program Files\Java\jdk-13
PATH:将JAVA_HOME的bin目录附加到系统环境变量PATH上。
Path=%JAVA_HOME%\bin;
JDK工具
- java:实际上就是JVM,运行Java程序,就是启动JVM,然后就是让JVM执行指定的编译后的字节码
- javac: Java编译器,将Java源码文件(.java)编译成Java字节码文件
- jar:将一组.class文件打包成一个.jar文件,便于发布
- javadoc:将Java源码中自动提取注释生成文档
- jdb: Java调试器,用于开发阶段的运行调试
Java程序示例
public class Hello{ public static void main(String[] args){ System.out.println("Hello, world!"); } }
一个Java源码只能定义一个public类型的class,并且class名称要和文件名一致。
基本数据类型
- 整数类型: byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型: boolean
Java只定义了带符号整型
,依次占用1,2,4,8字节,特殊定义方式:
long l=9000L; float f=3.14f; //需要强制加后缀
float最大表示3.4×10^38^,double表示1.79×10^308^。
字符类型char表示ASCII
外,还可以表示一个Unicode
字符。
var关键字
省略变量类型:
var sb=new StringBuilder();
运算优先级
() ! ~ ++ - * / % //两个整数相除只能得到结果的整数部分 + - << >> >>> & | += -= *= /=
- 整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而
溢出不会出错
,得到的结果有问题。 - 无符号右移>>>的符号位跟着动,而>>对于负数,其符号位不变;<<则可能正数变成负数。
类型转换
类型自动提升
强制类型转换
int i = 12345; short s = (short) i; // 12345
浮点数特殊值
- NaN: 表示Not a Number
- Infinity
- -Infinity
短路运算&&和||
字符和字符串
字符类型
char c1='A'; char c2='中'; int n1='A'; //65 int n2='中'; //20013 char c3='\u0041'; //'A'十六进制
字符串类型
String s="abc\"xyz"; String s2="Hei"+s; //字符串连接 //"""...."""表示多行字符串
不可变特性
:字符串的不可变指字符串内容不可变,而是引用可变。String s = "hello"; String t = s; s = "world"; System.out.println(t); // t是"hello"
空值null:该变量不指向任何对象。
数组
- 数组元素初始化为默认值,整型为0,浮点型为0,布尔型为false
数组一旦创建后,大小就不可改变
- 数组变量.length获取数组大小
- 数组元素可以是值类型(如int)或引用类型(如String),但数组本身是
引用类型
int[] ns; ns=new int[]{68,79,91,85,62}; ns=new int[]{1,2,3};
实际上是改变了引用对象
,数组大小不可变。
字符串数组
String[] names={"ABC","XYZ";"zoo"};
输入和输出
System.out.println() ; //输出并换行,还有print() //格式话输出 System.out.printf("%.2f\n", d); // 显示两位小数3.14
import java.util.Scanner; Scanner sc=new Scanner(System.in); //System.out 代表标准输出流,而 System.in 代表标准输入流 String name = sc.nextLine(); // 读 取 一 行 输 入 并 获 取 字 符 串 int age=sc.nextInt(); //整数 、nextDouble
变量相等判断
- ==:值变量是否相等、或者引用变量是否一致
- equals:判断引用变量的内容是否一致,避免空对象(NullPointerException)if ("hello".equals(s)) { ... }。
switch新的语法
switch判断字符串时,按字符串内容相等判断。
int opt = switch (fruit) { //可以用来赋值 case "apple" -> 1; case "pear", "mango" -> 2; default -> 0; }; // 注意赋值语句要以;结束 default -> { //还可以这样写 int code = fruit.hashCode(); yield code; // switch语句返回值 }
switch的计算结果必须是整型(char)、字符串、字符类型或枚举类型
开启预览:
javac --source 13 --enable-preview Main.java。
for each 写法
for each 循环能够遍历所有“可迭代”的数据类型,包括后面会介绍的 List 、 Map 等。
for (int n : ns) { System.out.println(n); } //打印二维数组,如下 for (int[][] arr : ns) { for (int n : arr) { System.out.print(n); System.out.print(', '); } System.out.println(); }
或者使用Java标准库:
import java.util.Arrays; int[] ns={3,2,1}; Arrays.toString(ns); //Arrays.deepToString()可打印多维数组 Arrays.sort(ns);
命令行参数
命令行参数是String[] 数组
for (String arg : args) { System.out.println(arg); }
面向对象编程
类和实例
- class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型;
- 而instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同。
this变量
在方法内部,this始终指向当前实例;如果没有命名冲突,可以省略this。若有局部变量和字段重名,那么局部变量优先级更高,就必须使用this。
方法的可变参数
可变参数用类型...定义,可变参数相当于数组类型,而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null,不需要提前构造数组.
public void setNames(String... names){ this.names=names; }
参数的传递
- 基本类型:值传递,调用方值的赋值
- 引用类型:调用方的变量和接受方的参数变量,指向同一个对象,修改会有影响
方法(Method)
- 方法可以让外部代码安全地访问实例字段
- public和private修饰的方法均可以访问private变量
- 方法是一组执行语句,并且可以执行任意逻辑
- 方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同)
- 外部代码通过public方法操作实例,内部代码可以调用private方法
- 理解方法的参数绑定,基本类型参数和引用类型参数
构造方法(Constructor)
没有在构造方法中初始化字段时,引用类型字段默认为null,数值类型用默认值。
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } }
实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例
没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
可以定义多个构造方法,编译器根据参数自动判断(其他方法存在是,不默认创建无参方法)
可以在一个构造方法内部调用另一个构造方法,便于代码复用
重载(Overload)
方法名相同,但各自的参数不同,方法重载的返回值类型通常都是相同的。
继承(Extends)
Protected:继承有个特点,就是**子类无法访问父类的private字段或者private方法。可修改为protected,此关键字可以把字段和方法的访问权限控制在继承树中。
Super关键字
class Student extends Person { protected int score; public Student(String name, int age, int score) { super(name, age); // 调 用 父 类 的 构 造 方 法 Person(String, int) this.score = score; } }
引用变量的转型
向上转型
Student s = new Student(); Person p = s; // upcasting, ok Object o1 = p; // upcasting, ok
instanceof()判断一个变量所指向的实例是否为指定类型,或者这个类型的子类。在向下转型时需要判断。
向下转型
Person p1 = new Student(); // upcasting, ok Person p2 = new Person(); Student s1 = (Student) p1; // ok Student s2 = (Student) p2; // runtime error! ClassCastException!
多态(Polymorphism)
Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值 也相同,就是 Override 。
多态:针对某个类型的方法调用,真正执行的方法取决于运行时期实际类型的方法。
Person p=new Student(); p.run(); //无法确定
覆写Object方法:
- toString():把instance输出为String
- equals():判断两个instance是否相等
- hasCode():计算一个instance的哈希值
final标记: 方法不能被覆写,类不能被继承,字段在初始化后不能被修改.
抽象编程
抽象类:如果一个类定义了方法,但没有具体执行代码,这个方法就是抽象方法,这个类也必须声明为抽象类。
抽象编程:尽量引用高层类型,避免引用实际子类型的方式
- 上层代码只定义规范
- 不需要子类就可以实现业务逻辑(正常编译)
- 具体业务逻辑由不用的子类实现,调用者并不关心
接口(Interface)
接口都是抽象方法,省略public abstract
。
抽象类和接口的对比:
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | × |
抽象方法 | √ | √ |
非抽象方法 | √ | 可以定义default方 |
List list = new ArrayList(); // 用List接口引用具体子类的实例 Collection coll = list; // 向上转型为Collection接口 Iterable it = coll; // 向上转型为Iterable接口
接口有default方法,无法访问字段,但是可以在有需要的子类处覆写此方法.
interface Person{ String getName(); //public abstract default void run(){ System.out.println(getName()+" run"); } }
实现可以不必要覆写default方法。
静态字段和方法
class Person{ public static int number; public static void setNumber(int value){ number=value; } }
静态方法属于 class 而不属于实例,因此,静态方法内部,无法访问 this 变量,也无法访问实例字段,它只能访问静态字段。
接口的静态字段: interface 是可以有静态字段的,并且静态字段必须为 final 类 。
public interface Person{ public static final int MALE=1; public static final int FEMALE=2; }
接口字段可以省略public static final;调用静态方法不需要实例,无法访问this,但可以访问其他静态字段和其他静态方法。
包(Package)
包没有父子关系:java.util和java.util.zip是不同的包,两者没有任何继承关系。
包作用域:位于同一包的类,可以访问作用域的字段和方法。不用 public 、 protected 、 private 修饰的字段和方法就是包作用域。
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。
- org.apache
- org.apache.commons.log
作用域
- 定义为public的class、interface可以被其他任何类访问;定义为public的field、method可以被其他类访问,前提是首先有访问class的权限
- 定义为private的field、method无法被其他类访问;定义在一个class内部的class称为嵌套类(nested class),嵌套类拥有访问private的权限
- protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类
- 包作用域是指一个类允许访问同一个package没有
public、private修饰的class
,以及没有public、protected、private修饰的字段和方法
classpath和jar
JVM通过环境变量classpath决定搜索class的路径和顺序;不推荐设置系统环境变量classpath,始终建议通过-cp命令传入;
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello #默认为.,即当前路径
jar包还可以包含一个特殊的 /META-INF/MANIFEST.MF 文件, MANIFEST.MF 是纯文本,可以指定 Main-Class 和其它信息。JVM会自动读 取这个 MANIFEST.MF 文件,如果存在 Main-Class ,我们就不必在命令行指定启动类名。
模块(Module)
JVM自带标准库rt.java,编译传入依赖jar包,JVM自带的标准库rt.jar不能传入classpath中,会干扰JVM运行:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
漏写某个运行时的jar会在运行期极有可能抛出ClassNotFoundException,从Java9开始,原有的Java库被拆分为模块,以.jmod在$JAVA_HOME/jmods下:
java.base.jmod java.compiler.jmod java.datatransfer.jmod java.desktop.jmod
模块名就是文件名,模块之间的依赖关系被写入到模块内的module-info.class,只有java.base不依赖任何模块。
src 目录下多了一个 moduleinfo.java 这个文件,这就是模块的描述文件。
module hello.world { //模块名 requires java.base; // 可不写,任何模块都会自动引入java.base requires java.xml; }
必须 声明依赖后,才能使用引入的模块
package com.itranswarp.sample; // 必须引入java.xml模块后才能使用其中的类: import javax.xml.XMLConstants; public class Main { public static void main(String[] args) { Greeting g = new Greeting(); System.out.println(g.hello(XMLConstants.XML_NS_PREFIX)); } }
javac -d bin src/module-info.java src/com/itranswarp/sample/*.java #编译,指定目录bin jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin . #打包bin目录下的所有class文件 jmod create --class-path hello.jar hello.jmod #创建jmod java --module-path hello.jar --module hello.world #运行
上述也说明,class是类容器;.jmod不能放入--module-path。
打包jre
jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/ # 在自己的模块中加入系统模块,并指定输出目录 jre/bin/java --module hello.world #运行
访问权限
如果外部代码想要访问 hello.world 模块中 的 com.itranswarp.sample.Greeting 类:
module hello.world { exports com.itranswarp.sample; //可供外部使用 requires java.base; requires java.xml; }
Java核心类
字符串(String)
字符串内部是通过一个char[]数组表示的,以下方法可以:
String s=new String(new char[]{'H','e','l','l','o'});
Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的 rivate final char[] 字段,以及没有任何修改 char[]
的 方法实现的。==
"Hello".indexOf('l'); //2 "Hello".contains("ll"); //true " \tHello\r\n".trim(); //移除字符串首尾空白字符 "\u3000Hello\u3000".strip(); // "Hello" ,类似中文字符也会被移除 " Hello ".stripLeading(); // "Hello " " Hello ".stripTrailing(); // " Hello" "".isEmpty(); // true,因为字符串长度为0,判断字符串是否为空 " ".isEmpty(); // false,因为字符串长度不为0 " \n".isBlank(); // true,因为只包含空白字符,判断是否为空白字符串 " Hello ".isBlank(); // false,因为包含非空白字符 s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w' String s = "A,,B;C ,D"; s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D" String[] ss = s.split("\\,"); // {"A", "B", "C", "D"} String[] arr = {"A", "B", "C"}; String s = String.join("***", arr); // "A***B***C",拼接字符串
类型转换
String.valueOf(123); // "123" String.valueOf(45.67); // "45.67" String.valueOf(true); // "true" String.valueOf(new Object()); // 类似java.lang.Object@636be97c int n1 = Integer.parseInt("123"); // 123 int n2 = Integer.parseInt("ff", 16); // 按 十 六 进 制 转 换 , 255 boolean b1 = Boolean.parseBoolean("true"); // true boolean b2 = Boolean.parseBoolean("FALSE"); // false Integer.getInteger("java.version"); // 版本号,11,该字符串对应的系统变量转换 为 Integer
转换为char[]
char[] cs="Hello".toCharArray(); String s=new String(cs); //cs值复制进入,而不是引用
字符编码
英文字符 'A' 的 ASCII 编码和 Unicode 编码分别为41和0041(十六进制),中文字符中的GB2312编码和Unicode编码分别为d6d0和4e2d。
UTF-8 编码,它是一种变长编码,用来把固定长度的 Unicode 编码变成1~4字节的变长编码。通过 UTF-8 编码,英文字 符 'A' 的 UTF-8 编码变为 0x41 ,正好和 ASCII 码一致,而中文 '中' 的 UTF-8 编码为3字节 0xe4b8ad 。
在Java中, ==char 类型实际上就是两个字节的 Unicode 编码==。
import java.nio.charset.StandardCharsets; byte[] b1="Hello".getBytes(); byte[] b2="Hello".getBytes("UTF-8"); byte[] b3="Hello".getBytes("GBK"); byte[] b4="Hello".getBytes(StandardCharsets.UTF_8); String s1=new String(b1,"GBK");
而较新的JDK版本的 ==String 则以 byte[] 存储==:如果 String 仅包含ASCII字符,则每个 byte 存储一个字符,否则,每两个 byte 存储一 个字符,这样做的目的是为了节省内存,因为大量的长度较短的 String 通常仅包含ASCII字符:
public final class String { private final byte[] value; private final byte coder; // 0 = LATIN1, 1 = UTF16 }
StringBuilder
StringBuilder是可变对象,用来高效拼接字符串;StringBuilder可以支持链式操作,实现链式操作的关键是返回实例本身(this);StringBuffer(使用同步)是StringBuilder的线程安全版本,现在很少使用。
StringJoiner
String[] names={"Li","Jin","Xi"}; var sj=new StringJoiner(",","Hello","!"); //指定分隔符,开头和结尾 for(String name: names){ sj.add(name); }
StringJoiner 内部实际上就是使用了 StringBuilder ,所 以拼接效率和 StringBuilder 几乎是一模一样的。
String[] names = {"Bob", "Alice", "Grace"}; var s = String.join(", ", names); //静态方法
包装类型
Java数据类型两种
基本类型:byte,short,int,long,boolean,float,double,char
引用类型:所有class和interface类型, 引用类型可以赋值为null,表示空,但基本类型不能赋值为null.
Integer n=100; //自动装箱 使用Integer.valueOf(int) int x=n; //自动拆箱 使用Integer.intValue()
只发生在编译阶段,会影响代码执行效率,因为编译后的class文件严格区分基本类型和引用类型.自动拆箱执行时会包nullPointerException对两个Integer实例进行比较要特别注意:绝对不能用==
比较,因为Integer是引用类型,必须使用equals()比较。
==把能创建“新”对象的静态方法称为静态工厂方法==。 Integer.valueOf() 就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
JavaBean
class定义符合以下规范:若干private实例字段;通过public方法来读写实例字段。
把一组对应的读方法 getter 和写方法 setter 称为属性property :
public class Person { private String name; private int age; public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性 的代码,主要用在图形界面的可视化设计中。
枚举JavaBean的所有属性:
import java.beans.*; BeanInfo info=Introspector.getBeanInfo(Person.class); for(PropertyDescriptor pd: info.getPropertyDescriptors()){ System.out.println(pd.getName()); System.out.println(" " + pd.getReadMethod()); System.out.println(" " + pd.getWriteMethod()); }
Enum
enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT; }
- enum 常量本身带有类型信息,即 Weekday.SUN 类型是 Weekday ,编译器会自动检查出类型错误
- 不可能引用到非枚举的值,因此无法通过编译
- 不同类型的枚举不能互相比较或者赋值,因为类型不符
==Enum枚举类型是一种引用类型,但依然可以使用比较,因为enum类型的每个常量在JVM中只有唯一实例。==
enum定义的类型就是class:
定义的enum类型总是继承自java.lang.Enum,且无法被继承;
只能定义==出enum的实例==,而无法通过new操作符创建enum的实例;
定义的每个实例都是引用类型的唯一实例;
可以将enum类型用于switch语句。
public enum Color { RED, GREEN, BLUE; } //编译后如下 public final class Color extends Enum { // 继承自Enum,标记为final class // 每个实例均为全局唯一: public static final Color RED = new Color(); //实例 public static final Color GREEN = new Color(); public static final Color BLUE = new Color(); // private构造方法,确保外部无法调用new操作符: private Color() {} //实例 }
String s=Weekday.SUN.name(); //"SUM" int n=Weekday.MON.ordinal(); //1 定义的常量的顺序,从0开始计数
可以定义 private 的构造方法,并且,给每个枚举常量添加字段:
enum Weekday{ MON(1,"星期一"),TUE(2,"星期二")...; public final int dayValue; private final String chinese; private Weekday(int dayValue, String chinese) { this.dayValue = dayValue; this.chinese = chinese; } @Override public String toString(){ return this.chinese; } }
BigInteger
java.math.BigInteger 就是用来表示任 意大小的整数。 BigInteger 内部用一个 int[] 数组来模拟一个非常大的整数:
BigInteger i1 = new BigInteger("1234567890"); BigInteger i2 = new BigInteger("12345678901234567890"); BigInteger sum = i1.add(i2); // 12345678902469135780
如果 BigInteger 表示的范围超过了基本类型的范围,转换时将丢失高位信 息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用 intValueExact() 、 longValueExact() 等方法,在转换时如果 超出范围,将直接抛出 Arithmetic Exception 异常。
BigDecimal
BigDecimal d1 = new BigDecimal("123.4500"); BigDecimal d2 = d1.stripTrailingZeros(); System.out.println(d1.scale()); // 4 System.out.println(d2.scale()); // 2, 因 为 去 掉 了 00 BigDecimal d3 = new BigDecimal("1234500"); BigDecimal d4 = d3.stripTrailingZeros(); System.out.println(d3.scale()); // 0 System.out.println(d4.scale()); // -2
设置精确度
BigDecimal d1 = new BigDecimal("123.456789"); BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568 BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
对 BigDecimal 做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
BigDecimal d1 = new BigDecimal("123.456"); BigDecimal d2 = new BigDecimal("23.456789"); BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保 留 10 位 小 数 并 四 舍 五 入 BigDecimal[] dr = n.divideAndRemainder(m); //dr[0]表示商,dr[1]表示余数
比较BigDecimal:在比较两个 BigDecimal 的值是否相等时,要特别注意,使用 equals() 方法不但要求两个 BigDecimal 的值相等,还要求它们 的 scale() 相等;应当使用compareTo()比较两个BigDecimal的值,不要使用equals()。这是因为:
public class BigDecimal extends Number implements Comparable<BigDecimal>{ private final BigInteger intVal; private final int scale; }
常用工具类
Math.abs(-100) = 100 //绝对值 Math.abs(-100) = 100 Math.exp(2) = 7.38905609893065 //求幂 Math.log(4) = 1.3862943611198906 //取对数 Math.log10(100) = 2.0 Math.sin(3.14) = 0.0015926529164868282 //三角 Math.PI = 3.141592653589793 //常量 Math.E = 2.718281828459045 Math.random() = 0.7907390688744504 //[0-1) //StrictMath可以保证所有平台计算结果相同 //伪随机数 Random r=new Random(123456); //种子 r.nextInt(10); //[0,10) r.nextDouble(); r.nextFloat(); r.nextLong(); //真随机数 SecureRandom sr=new SecureRandom(); //安全随机数 sr.nextInt(); byte[] buffer=new byte[16]; sr.nextBytes(buffer); //安全随机数填充 System.out.println(Arrays.toString(buffer)); //[69, 5, 14, 49, 89, 89, 9, 24, 89, -50, -53, -87, 103, 54, -31, -2] //SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数. //这个种子是通过CPU//的热噪声,读写磁盘的字节,网络流量等各种随机事件产生的"熵".
异常处理
异常继承关系
Throwable 是异常体系的根,它继承自 Object 。 Throwable 有两个体系: Error 和 Exception , Error 表示严重的 错误,程序对此一般无能为力。
Error
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
Exception
- 应用程序逻辑的一部分 ,可以被捕获并处理
- NumberFormatException:数值类型的格式错误,
- FileNotFoundException:未找到文件,
- SocketException:读取网络失败,
- 程序逻辑编写不对,应该修复程序本身
- NullPointerException:对某个null的对象调用方法或字段,
- IndexOutOfBoundsException:数组索引越界.
- 应用程序逻辑的一部分 ,可以被捕获并处理
Exception又分为两大类:
- RuntimeException以及它的子类:
- 非RuntimeException(IOException,ReflectiveOperationException等)
Java中要求:
- ==必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception.==
- 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类
捕获异常
使用try ... catch ... finally时:多个catch语句的匹配顺序非常重要,子类必须放在前面;finally语句保证了有无异常都会执行,它是可选的;
一个catch语句也可以匹配多个非继承关系的异常.
Exception │ ├─ RuntimeException │ │ │ ├─ NullPointerException │ │ │ ├─ IndexOutOfBoundsException │ │ │ ├─ SecurityException │ │ │ └─ IllegalArgumentException │ │ │ └─ NumberFormatException │ ├─ IOException │ │ │ ├─ UnsupportedCharsetException │ │ │ ├─ FileNotFoundException │ │ │ └─ SocketException │ ├─ ParseException │ ├─ GeneralSecurityException │ ├─ SQLException │ └─ TimeoutException
public static void main(String[] args) { try { process1(); } catch (IOException | NumberFormatException e) { // IOException 或 NumberFormatException System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }
抛出异常
public static void main(String[] args){ try{ process1(); }catch(Exception e){ e.printStackTrace(); //打印出方法的调用栈 } static void process1(){ process2(); } static void process2(){ Integer.parseInt(null); } }
public static int parseInt(String s, int radix) throws NumberFormatException { if (s == null) { throw new NumberFormatException("null"); } ... }
如果一个方法捕获了某个异常后,又在 catch 子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:
void process1(String s) { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(); //throw new IllegalArgumentException(e) ;记录了原始信息 } } void process2(String s) { if (s==null) { throw new NullPointerException(); } }
finally 抛出异常后,原来在 catch 中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异 常(Suppressed Exception)。
finally { Exception e = new IllegalArgumentException(); if (origin != null) { e.addSuppressed(origin); } throw e; }
通过 Throwable.getSuppressed() 可以获取所有的 Suppressed Exception 。
自定义异常
自定义异常体系时,推荐从 RuntimeException 派生“根异常”,再派生出业务异常;自定义异常时,应该提供多种构造方法。
使用断言
public static void main(String[] args) { double x = Math.abs(-123.45); assert x >= 0:"x >= 0"; System.out.println(x); }
语句 assert x >= 0; 即为断言,断言条件 x >= 0 预期为 true 。如果计算结果为 false ,则断言失败,抛出 AssertionError 。
要执行 assert 语句,必须给Java虚拟机传递 -enableassertions (可简写为 -ea )参数启用断言。
使用JDK Logging
使用日志的好处:
- 可以设置输出样式,避免自己每次都写 "ERROR: " + var ;
- 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
import java.util.logging.Level; import java.util.logging.Logger; public static void main(String[] args){ Logger logger=Logger.getGlobal(); logger.info("start process ..."); logger.warning("memory is running out ..."); logger.fine("ignored."); logger.severe("process will be terminated ..."); }
共七个级别:SEVERE 、WARNING 、INFO、 CONFIG 、FINE、 FINER 、FINEST;FINE以下的级别不会显示出来。
在JVM启动时传递参数:
-D java.util.logging.config.file=<config-file-name>
使用Commons Logging
Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。它可以挂接不同的日志系统,默认使用Log4j,没有则使用JDK Logging。
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Main{ public static void main(String[] args){ Log log=LogFactory.getLog(Main.class); log.info("start..."); log.warn("end."); } }
java -cp .;commons-logging-1.2.jar Main
定义了六个级别:FATAL 、ERROR 、WARNING 、INFO 、DEBUG、 TRACE,默认级别为INFO。
使用 log.error(String, Throwable) 打印异常
使用Log4j
log-->Appender-->Filter-->Layout-->Console|File|Socket|jdbc
要打印日志,只需要按Commons Logging的写法写,不需要改动任何代码,就可以得到Log4j的日志输出。
使用SLF4J和Logback
log.info("Set score " + score + " for Person " + p.getName() + " ok."); //Commons Logging logger.info("Set score {} for Person {} ok.", score, p.getName()); //SLF4J
import org.slf4j.Logger; import org.slf4j.LoggerFactory; class Main { final Logger logger = LoggerFactory.getLogger(getClass()); }
SLF4J和Logback可以取代Commons Logging和Log4j;始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。
反射
在程序运行期拿到一个对象的所有信息,反射是为了解决在运行期对某个实例一无所知的情况下,如何调用其方法,通过Class实例获取class信息的方法称为反射。类名、包名、父类、实现的接口、所有方法、字段等。
Class类
class是由JVM在执行在过程中动态加载的。JVM第一次读取到一种class类型时,将其加载进内存,没加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。
public final class Class{ private Class(){} }
以String类为例,当JVM加载String类,它首次读取String.class文件到内存,然后为String类创建一个Class实例:
Class cls=new Class(String);
Class构造方法是private,只有JVM能创建Class实例,所以JVM持有的每个Class实例都指向一个数据类型(class或interface)。
获取class的Class实例:
方法一:通过class的静态变量获取
Class cls=String.class;
方法二:通过该实例变量提供的getClass()方法获取
String s="hello"; Class cls=s.getClass();
方法三:通过静态方法Class.forName()获取
Class cls=Class.forName("java.lang.String");
==由于Class实例在JVM中是唯一的,可以用 ==比较两个实例==
Class cls1=String.class; String s="hello; Class cls2=s.getClass(); boolean sameClass=cls1==cls2; //true
用instanceof不但匹配当前类型,还匹配当前类型的子类;而用==判断class实例可以精确地判断数据类型,但不能作子类型比较。
注意到数组(例如 String[] )也是一种 Class ,而且不同于 String.class ,它的类名是 [Ljava.lang.String 。此外,JVM为每一种 基本类型如int也创建了 Class ,通过 int.class 访问。
根据Class实例来创建对应类型实例:
Class cls=String.class; String s=(String)cls.newInstance(); //只能调用 public 的无参数构造方法
JVM总是动态加载class,可以在运行期根据条件来控制加载class。
访问字段
获取字段信息:
Field getField(name):根据字段名获取某个public的field(包括父类)
Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
Field[] getFields():获取所有public的field(包括父类)
Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
public final class String { private final byte[] value; } Field f=String.class.getDeclaredField("value"); f.getName(); // "value" f.getType(); // class [B 表示 表示byte[]类型 类型 int m = f.getModifiers(); Modifier.isFinal(m); // true Modifier.isPublic(m); // false Modifier.isProtected(m); // false Modifier.isPrivate(m); // true Modifier.isStatic(m); // false
==Field.get(Object) 获取指定实例的指定字段的值。==若是private,则需要Field.setAccessible(true) ,显然破坏了类的封装。
设置字段值是通过 Field.set(Object, Object) 实现的,其中第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。
调用方法
Method getMethod(name, Class...):获取某个public的Method(包括父类)
Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
Method[] getMethods():获取所有public的Method(包括父类)
Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
通过Method实例可以获取方法信息:getName(),getReturnType(),getParameterTypes(),getModifiers();通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters);
String s="Hello world"; Method m=String.class.getMethod("substring",int.class); String r=(String)m.invoke(s,6); //在S对象上调用该方法获取结果
调用静态方法:
Method m = Integer.class.getMethod("parseInt", String.class); // 调用该静态方法并获取结果: Integer n = (Integer) m.invoke(null, "12345");
调用非public方法时,设置setAccessible(true)来访问非public方法;通过反射调用方法时,仍然遵循多态原则。
获取构造方法
getConstructor(Class...):获取某个public的Constructor;
getDeclaredConstructor(Class...):获取某个Constructor;
getConstructors():获取所有public的Constructor;
getDeclaredConstructors():获取所有Constructor。
注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。
Constructor cons2 = Integer.class.getConstructor(String.class); Integer n2 = (Integer) cons2.newInstance("456");
获取继承关系:
- Class getSuperclass():获取父类类型;
- Class[] getInterfaces():获取当前类实现的所有接口。
通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
Number.class.isAssignableFrom(Integer.class); // true , 因 为 Integer 可 以 赋 值 给 Number
动态代理
class和interface的区别:可以实例化class(非abstract);不能实例化interface;所有的interface类型变量总是通过向上转型并指向某个实例.
CharSequence cs=new StringBuilder();
可以在运行期动态创建某个interface的实例,而不必编写实现类,这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。
import java.lang.refect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface hello { void morning(String name); } public static void main(String[] args){ InnovactionHandler handler=new InnovcationHander(){ @Ovverride public Object invoke(Object proxy, Method method, Object[] args){ System.out.println(method); if(method.getName().equals("morning")){ System.out.println("Good morning, "+args[0]); } return null; } }; Hello hello=(Hello)Proxy.newProxyInstance( Hello.class.getClassLoader(), //传入ClassLoader new Class[]{hello.class}, //传入要实现的接口 handler); hello.morning("Li"); }
注解
注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据。
- 第一类是由编译器使用的注解,例如:
@Override:让编译器检查该方法是否正确地实现了覆写;
@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。 - 第二类是由工具处理.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
- 第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了 @PostConstruct的方***在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
定义一个注解时,还可以定义配置参数。配置参数可以包括:所有基本类型;String;枚举类型;基本类型、String以及枚举的数组。因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。此外,大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。如果只写注解,相当于全部使用默认值。
定义注解
Java使用@interface定义注解:
- 可定义多个参数和默认值,核心参数使用value名称;
- 必须设置@Target来指定Annotation可以应用的范围;
- 应当设置@Retention(RetentionPolicy.RUNTIME)便于运行期读取该Annotation。
第一步,用@interface定义注解:
public @interface Report { }
第二步,添加参数、默认值:
public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。
第三步,用元注解(可以修饰其他注解)配置注解:
@Target(ElementType.TYPE) //定义 Annotation 能够被应用于源码的哪些位置 @Retention(RetentionPolicy.RUNTIME) //默认为CLASS public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
其中,必须设置@Target和@Retention,@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited和@Repeatable。
处理注解
根据@Retention的配置:
- SOURCE类型的注解在编译期就被丢掉了;
- CLASS类型 的注解仅保存在class文件中,它们不会被加载进JVM;
- RUNTIME类型的注解会被加载进JVM,并且在运行期可以被程序读取
读取注解
因为注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射API。
Java提供的使用反射API读取Annotation的方法包括:
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();
可以在运行期通过反射读取RUNTIME类型的注解,注意千万不要漏写@Retention(RetentionPolicy.RUNTIME),否则运行期无法读取到该注解。
可以通过程序处理注解来实现相应的功能:
- 对JavaBean的属性值按规则进行检查;
- JUnit会自动运行@Test标记的测试方法
@Rentation(RentationPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Range{ int min() default 0; int maxdefault 255; }
public class Person{ @Range(min=1,max=20) public String name; @Range(max=10) public String city; }
void check(Person person) throws IllegalArgumentException,ReflectiveOperationException{ for(Field field: person.getClass().getFields()){ Range range=field.getAnnotation(Range.class); if(range!=null){ Object value = filed.get(person); if(value instanceof String){ String s=(String)value; if(s.length()<range.min()||s.length>range.max()){ throw new IllegalArgumentException("Invalid field: "+filed.getName); } } } } }
泛型
什么是泛型
泛型就是编写模板代码来适应任意类型;
泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
注意泛型的继承关系:
可以把ArrayList< Integer>向上转型为List< Integer >(T不能变!),但不能把ArrayList< Integer>向上转型为ArrayList< Number>(T不能变成父类)。
public class ArrayList<T> { private T[] array; private int size; public void add(T e) {...} public void remove(int index) {...} public T get(int index) {...} } // 创建可以存储String的ArrayList: ArrayList<String> strList = new ArrayList<String>(); // 创建可以存储Float的ArrayList: ArrayList<Float> floatList = new ArrayList<Float>(); // 创建可以存储Person的ArrayList: ArrayList<Person> personList = new ArrayList<Person>();
// 可以省略后面的Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();
泛型接口
把泛型参数< T>替换为需要的class类型,例如:ArrayList< String>,ArrayList< Number>等;可以省略编译器能自动推断出的类型,例如:List< String> list = new ArrayList<>();不指定泛型参数类型时,编译器会给出警告,且只能将< T>视为Object类型;可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。
public interface Comparable<T> { /** * 返 回 -1: 当 前 实 例 比 参 数 o 小 * 返 回 0: 当 前 实 例 与 参 数 o 相 等 * 返 回 1: 当 前 实 例 比 参 数 o 大 */ int compareTo(T o); }
编写泛型
编写泛型时,需要定义泛型类型< T>;静态方法不能引用泛型类型< T>,必须定义其他类型(例如< K>)来实现静态泛型方法;泛型可以同时定义多种类型,例如Map<K, V>。
public class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { ... } public T getLast() { ... } // 静 态 泛 型 方 法 应 该 使 用 其 他 类 型 区 分 : public static <K> Pair<K> create(K first, K last) { return new Pair<K>(first, last); } }
檫拭法
Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
- 编译器把类型< T>视为Object
- 编译器根据< T>实现安全的强制转型。
局限一:< T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型
Pair<int> p = new Pair<>(1, 2); // compile error!
局限二:无法取得带泛型的Class,因为T是Object,我们对Pair< String>和Pair< Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。
局限三:无法判断带泛型的Class
Pair<Integer> p = new Pair<>(123, 456); // Compile error: if (p instanceof Pair<String>.class) { }
局限四:不能实例化T类型
public class Pair<T> { private T first; private T last; public Pair() { // Compile error: first = new T(); last = new T(); } } 上述代码无法通过编译,因为构造方法的两行语句: first = new T(); last = new T(); 擦拭后实际上变成了: first = new Object(); last = new Object(); 这样一来,创建new Pair<String>()和创建new Pair<Integer>()就全部成了Object, 显然编译器要阻止这种类型不对的代码
要实例化T类型,我们必须借助额外的Class< T>参数:
public class Pair<T> { private T first; private T last; public Pair(Class<T> clazz) { first = clazz.newInstance(); last = clazz.newInstance(); } }
上述代码借助Class< T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class< T>。例如:
Pair<String> pair = new Pair<>(String.class);
因为传入了Class< String>的实例,所以我们借助String.class就可以实例化String类型。
不恰当的覆写方法,例如:
public class Pair<T> { public boolean equals(T t) { return this == t; } }
这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。换个方法名,避开与Object.equals(Object)的冲突就可以成功编译
一个类可以继承自一个泛型类。例如:父类的类型是Pair< Integer>,子类的类型是IntPair,可以这么继承:
public class IntPair extends Pair<Integer> { }
获取父类泛型代码:
import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; Class<IntPair> clazz = IntPair.class; Type t = clazz.getGenericSuperclass(); if (t instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) t; Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型 Type firstType = types[0]; // 取第一个泛型类型 Class<?> typeClass = (Class<?>) firstType; System.out.println(typeClass); // Integer }
extends通配符
static int add(Pair<? extends Number> p){ Number first=p.getFirst(); Number last=p.getLast(); return first.intValue()+last.intValue(); } class Pair<T> { rivate T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } } public static void main(String[] args) { Pair<Integer> p = new Pair<>(123, 456); int n = add(p); System.out.println(n); }通配符作为方法参数时表示: - 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst(); - 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n); 即一句话总结:使用extends通配符表示可以读,不能写。 #### super通配符 ```java void set(Pair p, Integer first, Integer last) { p.setFirst(first); p.setLast(last); } ``` - 允许调用set(? super Integer)方法传入Integer的引用; - 不允许调用get()方法获得Integer的引用。唯一例外是可以获取Object的引用:Object o = p.getFirst()。 ```java public class Collections { // 把src的每个元素复制到dest中: public static <t> void copy(List dest, List src) { for (int i=0; i<src.size>
通配符既没有extends,也没有super,因此:不允许调用set(T)方法并传入引用(null除外);不允许调用T get()方法并获取T引用(只能获取Object引用)。
< ?>通配符有一个独特的特点,就是:Pair<?>是所有Pair< T>的超类
泛型和反射
部分反射API是泛型,例如:Class< T>,Constructor< T>:
Class<String> clazz=String.class; Class<? super String> sup = String.class.getSuperclass(); Constructor<Integer> cons=Integer.class.getConstructor(int.class);
可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型:
@SuppressWarnings("unchecked") Pair<String>[] ps=(Pair<String>[])new Pair[2];
可以通过Array.newInstance(Class< T>, int)创建T[]数组,需要强制转型:
T[] createArray(Class<T> cls){ return (T[])Array.newInstance(cls,5); }
还可以使用可变参数创建泛型数组:
public class ArrayHelper{ @SafeVarags static <T> T[] asArray(T... objs){ return objs; } } String[] ss = ArrayHelper.asArray("a", "b", "c");
同时使用泛型和可变参数时需要特别小心。
集合
什么是集合
在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,
我们把这种Java对象称为集合.
String[] ss = new String[10]; // 可以持有10个String对象 ss[0] = "Hello"; // 可以放入String对象 String first = ss[0]; // 可以获取String对象
数组有如下限制:
- 数组初始化后大小不可变;
- 数组只能按索引顺序存取。
Collection
List:一种有序列表的集合,例如,按索引排列的Student的List;
Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set;
Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map。
Java集合的设计有几个特点:
一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类ArrayList,LinkedList等,
二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
List< String> list = new ArrayList<>(); // 只能放入String类型最后,Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
遗留类:
- Hashtable :一种线程安全的 Map 实现;
- Vector :一种线程安全的 List 实现;
- Stack :基于 Vector 实现的 LIFO 的栈
使用List
List< E>接口主要的接口方法:
- 在末尾添加一个元素:void add(E e)
- 在指定索引添加一个元素:void add(int index, E e)
- 删除指定索引的元素:int remove(int index)
- 删除某个元素:int remove(Object e)
- 获取指定索引的元素:E get(int index)
- 获取链表大小(包含元素的个数):int size()
实现List接口并非只能通过数组(即ArrayList的实现方式)来实现,另一种LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │
通常情况下,我们总是优先使用ArrayList。
List的特点:
- List接口允许我们添加重复的元素,即List内部的元素可以重复
- List还允许添加null:(String)
创建List
List<Integer> list = List.of(1, 2, 5);
但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。
遍历List
可以用for循环根据索引配合get(int)方法遍历,但这种方式并不推荐,一是代码复杂,二是因为get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢,要始终坚持使用迭代器Iterator来访问List。
for (Iterator<String> it = list.iterator(); it.hasNext(); ) { String s = it.next(); System.out.println(s); } //Java的for each循环本身就可以帮我们使用Iterator遍历 for (String s : list) { System.out.println(s); }
List和Array转换
第一种是调用toArray()方法直接返回一个Object[]数组
Object[] array = list.toArray();
这种方***丢失类型信息。
第二种方式是给
toArray(T[])
传入一个类型相同的Array,List内部自动把元素复制到传入的Array中Integer[] array = list.toArray(new Integer[3]);
如果我们传入类型不匹配的数组,例如,String[]类型的数组,由于List的元素是Integer,所以无法放入String数组,这个方***抛出ArrayStoreException,如果传入的数组不够大,那么List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null。
Integer[] array = list.toArray(new Integer[list.size()]);
最后一种更简洁的写法是通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法:
Integer[] array = list.toArray(Integer[]::new);
反过来,把Array变为List就简单多了,通过List.of(T...)方法最简单。
Integer[] array = { 1, 2, 3 }; List<Integer> list = List.of(array);
对于JDK 11之前的版本,可以使用Arrays.asList(T...)方法把数组转换成List。要注意的是,返回的List不一定就是ArrayList或者LinkedList,因为List只是一个接口,如果我们调用List.of(),它返回的是一个只读List,对只读List调用add()、remove()方***抛出UnsupportedOperationException。
编写equals方法
equals() 方法要求我们必须满足以下条件:
- 自反性(Reflexive):对于非 null 的 x 来说, x.equals(x) 必须返回 true ;
- 对称性(Symmetric):对于非 null 的 x 和 y 来说,如果 x.equals(y) 为 true ,则 y.equals(x) 也必须为 true ;
- 传递性(Transitive):对于非 null 的 x 、 y 和 z 来说,如果 x.equals(y) 为 true , y.equals(z) 也为 true ,那 么 x.equals(z) 也必须为 true ;
- 一致性(Consistent):对于非 null 的 x 和 y 来说,只要 x 和 y 状态不变,则 x.equals(y) 总是一致地返回 true 或者 false ;
- 对 null 的比较:即 x.equals(null) 永远返回 false 。
public boolean equals(Object o) { if (o instanceof Person) { Person p = (Person) o; return Objects.equals(this.name, p.name) && this.age == p.age; //省去了判断 null } return false; }
使用Map
Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。
- 当我们调用V get(K key)时,就可以通过key获取到对应的value。
- 如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap。
如果只是想查询某个key是否存在,可以调用boolean containsKey(K key)方法,put()方法的签名是V put(K key, V value),如果放入的key已经存在,put()方***返回被删除的旧的value,否则,返回null。
for(String kep:map.keySet()){ Integer value=map.get(key); } //或者 for(Map.entry<String,Integer> entry: map.entrySet()){ String key=entry.getKey(); String value=entry.getValue(); }
正确使用Map必须保证:
作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;
作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:
如果两个对象相等,则两个对象的hashCode()必须相等;
如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
int hashCode() { return Objects.hashCode(firstName, lastName, age); }
我们把不同的key具有相同的hashCode()的情况称之为哈希冲突。在冲突的时候,一种最简单的解决办法是用List存储hashCode()相同的key-value。显然,如果冲突的概率越大,这个List就越长,Map的get()方法效率就越低,这就是为什么要尽量满足条件二:如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
使用EnumMap
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。使用EnumMap的时候,根据面向抽象编程的原则,应持有Map接口。
使用SortedMap
SortedMap在遍历时严格按照Key的顺序遍历,最常用的实现类是TreeMap;作为SortedMap的Key必须实现Comparable接口,或者传入Comparator;要严格按照compare()规范实现比较逻辑,否则,TreeMap将不能正常工作。
使用Properties
Properties 内部本质上是一 个 Hashtable。读取配置文件:
Properties props=new Properties(); props.load(new java.io.FileInputStream("setting.properties")); //字节流 props.load(new FileReader("settings.properties", StandardCharsets.UTF_8)); //字符流,不涉及编码问题 String filepath=props.getProperty("last_open_file"); String interval = props.getProperty("auto_save_interval", "120"); props.setProperty("language", "Java"); props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
使用Set
Set用于存储不重复的元素集合,它主要提供以下几个方法:
将元素添加进Set< E>:boolean add(E e)
将元素从Set< E>删除:boolean remove(Object e)
判断是否包含元素:boolean contains(Object e)
放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set。
Set接口并不保证有序,而SortedSet接口则保证元素是有序的:
HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口
TreeSet是有序的,因为它实现了SortedSet接口
使用TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现
Comparable接口,
如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator
对象。
Set用于存储不重复的元素集合:
- 放入HashSet的元素与作为HashMap的key要求相同;
- 放入TreeSet的元素与作为TreeMap的Key要求相同;
- 利用Set可以去除重复元素;
使用Queue
Queue实际上是实现了一个先进先出(FIFO)的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:
- 把元素添加到队列末尾;
- 从队列头部取出元素。
方法有:
- int size():获取队列长度;
- boolean add(E)/boolean offer(E):添加元素到队尾;
- E remove()/E poll():获取队首元素并从队列中删除;
- E element()/E peek():获取队首元素但并不从队列中删除
不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。
// 这是一个List: List<String> list = new LinkedList<>(); // 这是一个Queue: Queue<String> queue = new LinkedList<>();
使用PriorityQueue
PriorityQueue和Queue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。要使用PriorityQueue,我们就必须给每个元素定义“优先级”。放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。如果我们要放入的元素并没有实现Comparable接口怎么办?
PriorityQueue允许我们提供一个Comparator对象来判断两个元素的顺序。
Deque双端队列(Double Ended Queue)
Java集合提供了接口Deque来实现一个双端队列,它的功能是:
- 既可以添加到队尾,也可以添加到队首;
- 既可以从队首获取,又可以从队尾获取。
对于添加元素到队尾的操作,Queue提供了add()/offer()方法,而Deque提供了addLast()/offerLast()方法。添加元素到对首、取队尾元素的操作在Queue中不存在,在Deque中由addFirst()/removeLast()等方法提供。注意到Deque接口实际上扩展自Queue,Queue提供的add()/offer()方法在Deque中也可以使用,但是,使用Deque,最好不要调用offer(),而是调用offerLast(),使用Deque,推荐使用offerLast(),offerFirst()或者pollFirst()/pollLast()方法。
==LinkedList即是List,又是Queue,还是Deque。==
// 不推荐的写法: LinkedList<String> d1 = new LinkedList<>(); d1.offerLast("z"); // 推荐的写法: Deque<String> d2 = new LinkedList<>(); d2.offerLast("z");
可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。
使用栈
- Stack只有入栈和出栈的操作:
- 把元素压栈:push(E)
- 把栈顶的元素“弹出”:pop(E)
- 取栈顶元素但不弹出:peek(E)
使用Iterator
class ReverseList<T> implements Iterable<T>{ private List<T> list=new ArrayList<>(); public void add(T t){ list.add(t); } @Override public Iterator<T> iterator(){ return new ReverseIterator(list.size()); } class ReverseIterator implements Iterator<T>{ int index; ReverseIterator(int index){ this.index=index; } @Override public boolean hasNext(){ return index>0; } @Override public T next(){ index--; return ReverseList.this.list.get(index); } } }
使用Collections
Collections提供了一系列方法来创建空集合:
创建空List: List< T> emptyList()
创建空Map: Map<K, V> emptyMap()
创建空Set: Set< T> emptySet()
要注意到返回的空集合是不可变集合,无法向其中添加或删除元素Collections提供了一系列方法来创建一个单元素集合:
创建一个元素的List: List< T> singletonList(T o)
创建一个元素的Map: Map<K, V> singletonMap(K key, V value)
创建一个元素的Set: Set< T> singleton(T o)
要注意到返回的单元素集合也是不可变集合,无法向其中添加或删除元素Collections可以对List进行排序。因为排序会直接修改List元素的位置,因此必须传入可变List Collections.sort(list);
Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌 Collections.shuffle(list);
Collections提供了一组方法把可变集合封装成不可变集合:
封装成不可变List: List< T> unmodifiableList(List<? extends T> list)
封装成不可变Set: Set< T> unmodifiableSet(Set<? extends T> set)
封装成不可变Map: Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
然而,继续对原始的可变List进行增删是可以的,并且,会直接影响到封装后的“不可变”List.Collections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:
变为线程安全的List: List< T> synchronizedList(List< T> list)
变为线程安全的Set: Set< T> synchronizedSet(Set< T> s)
变为线程安全的Map: Map<K,V> synchronizedMap(Map<K,V> m)
IO
IO定义
IO是指Input/Output,即输入和输出;IO流是一种顺序读写数据的模式,它的特点是单向流动。 数据类似自来水一样在水管中流动,所以我们把它称为IO流;IO流以byte(字节)为最小单位,因此也称为字节流;InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。
Reader和Writer表示字符流,字符流传输的最小数据单位是char;使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了编码,转换成char。
使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
FILE对象
File f=new File("..."); System.out.println(f.getPath());
- getPath(),返回构造方法传入的路径,
- getAbsolutePath(),返回绝对路径,
- getCanonicalPath(),它和绝对路径类似,但是返回的是规范路径。(不含.和..)
File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:
- boolean canRead():是否可读;
- boolean canWrite():是否可写;
- boolean canExecute():是否可执行;
- long length():文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录
当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。
File f=File.createTempFile("tmp-",".txt"); f.deleteOnExit(); //JVM退出时删除
当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。
listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:
public static void main(String[] args){ File f=new File("C:\\Windows"); File[] fs1=f.listFiles(); //列出所有文件和子目录 if(fs1!=null){ for(File fs: fs1){ System.out.println(fs); } } File[] fs2=f.listFile(new FilenameFilter(){ public boolean accept(File dir, String name){ return name.endswith(".exe"); } }); }
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
- boolean mkdir():创建当前File对象表示的目录;
- boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
- boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path对象
import java.io.*; import java.nio.file.*; Path p1=Paths.get(".","project","study") ; //构造一个对象 Path p2=p1.toAbsolutePath(); //转换为绝对路径 Path p3=p2.normalize(); //转换为规范路径 File f=p3.toFile(); //转换为File对象
InputStream
InputStream是一个抽象类,
public abstract int read() throws IOException; //读取一个字节,返回字节的int值
实现java.lang.AutoCloseable接口,就可以 try(resource = ...) ,编译器会自动加上finally语句,调用close()方法。
读入多个字节:
- int read(byte[] b):返回读取字节数
- int read(byte[] ,int off, int len):指定byte[]数组偏移量和最大填充数
public void readFile()throw IOException{ try(InputStream in=new FileInputStream("src/readme.txt")); byte[] buffer=new byte[100]; int n; while((n=in.read(buffer,0,100))!=-1){ //阻塞方法 .... } }
ByteArrayInputStream 可以在内存中模拟 一个 InputStream:
byte[] data={72,101,108,108,111,33}; try(InputStream in=new ByteArrayInputStream(data)){ int n; while((n=in.read())!=-1){ ... } }
OutputStream
public abstract void write(int b) throws IOException;
一次写入一个字节到输出流, flush() 方法,它的目的是将缓冲区的内容真正输出到目的。
public void writeFile() throws IOException{ try(OutputStream out=new FileOutputStream("out/readme.txt")){ out.write("Hello".getBytes("UTF-8")); //阻塞 } }
ByteArrayOutputStream 可以在内存中模 拟一个 OutputStream
byte[] data; try(ByteArrayOutputStream out=new ByteArrayOutStream()){ out.write("Hello".getBytes("UTF-8")); data=out.toByteArray(); }
Filter模式
JDK首先将InputStream分为两大类:
一类是直接提供数据的基础InputStream,例如:
FileInputStream ByteArrayInputStream ServletInputStream ...
一类是提供额外附加功能的InputStream,例如:
BufferedInputStream DigestInputStream CipherInputStream
通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合。
编写自定义Filter:
class CountInputStream extends FilterInputStream{ private int count=0; CountInputStream(InputStream in){ super(in); } public int read() throws IOException{ int n=in.read(); if(n!=-1){ this.count++; } return n; } pulic int read(byte[],int off,int len) throws IOException{ int n=in.read(b,off,len); this.count++; return n; } public int getBytesRead(){ return this.count; } }
操作ZIP
ZipInputStream
JarInputStream -> ZipInputStream-> InflaterInputStream -> FilterInputStream -> InputStream
其中JarInputStream从ZipInputStream派生,增加的功能是直接读取jar文件中的MANIFEST.MF。
//读取 try(ZipInputStream zip=new ZipInputStream(new FileInputStream(...))){ ZipEntry entry=null; //ZipEntry表示一个压缩文件或者目录 while((entry=zip.getNextEntry())!=null){ String name=entry.getName(); if(!entry.isDirectory()){ int n; while((n!=zip.read())!=-1){ ... } } } } //写入 try(ZipOutputStream zip=ZipOutputStream(new FileOutputStream(...))){ File[] files=... for(File f:files){ zip.putNextEntry(new ZipEntry(file.getName())); zip.write(getFileDataAsBytes(file)); zip.closeEntry(); } }
读取classpath资源
try(InputStream input=getClass().getResourseAsStream('/default.properties')){ if(intput!=null){ //资源文件不存在则返回null ... } } Properties props = new Properties(); props.load(inputStreamFromClassPath("/default.properties")); props.load(inputStreamFromFile("./conf.properties"));
序列化
序列化是指将一个Java对象变成二进制内容,及byte[]数组,这样Java对象便可以存储到文件或者网络传输出去; 反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象 。
要想序列化,需要实现java.io.Serializable接口。
public interface Serializable{ //没有定义任何方法,空接口,又称为标记接口 }
ByteArrayOutputStream buffer=new ByteArrayOutputStream(); try(ObjectOutputStream output=new ObjectOutputStream(buffer)){ output.writeInt(123); output.writeUTF("hello"); output.writeObject(Double.valueOf(123.456)); } System.out.print(Arrays.toString(buffer.toByteArray()));
反序列化
try(ObjectInputStream input=new ObjectInputStream(...)){ int n=input.readInt(); String s=intput.readUTF(); Double d=(Double)input.readObject(); }
readObject()可能抛出的异常有:
- ClassNotFoundException:没有找到对应的Class;
- InvalidClassException:Class不匹配。
public class Person implements Serializable { private static final long serialVersionUID = 2709425275741743919L; //阻止class不匹配 }
==反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。==
Reader
public int read() throwss IOException;
InputStream | Reader |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
读取字节(-1,0~255):int read() | 读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
FileReader
try(Reader reader=new FileReader('/readme.txt',StandardCharset.UTF_8)){ for(;;){ int n=reader.reader(); if(n==-1)breadk; System.out.println((char)n); } } //public int read(char[] c) throws IOException 一次读取多个字符 char[] buffer=new char[100]; while((n=reader.read(buffer))!=-1){ ... }
CharArrayReader
CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader.
try(Reader reader=new CharArrayReader("Hello".toCharArray())){ }
StringReader
StringReader可以直接把String作为数据源
try (Reader reader = new StringReader("Hello")) { }
InputStreamReader
Reader本质上是一个基于InputStream的byte到char的转换器 。
try(Reader reader=new InputStreamReader(new FileInputStream("/readme.txt"),"UTF-8")){ } //FileReader的一种实现方式,InputStreamReader是转换器
Writer
Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出 。
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) |
FileWriter
try(Writer witer=new FileWriter('readme.txt',StandardCharset.UTF_8)){ writer.write("H"); writer.write("Hello".toCharArray()); writer.write("Hello"); }
CharArrayWriter
try(CharArrayWriter writer=new CharArrayWriter()){ writer.write(65); writer.writer(66); writer.writer(67); char[] data=writer.toCharArray(); //{'A','B','C'} }
StringWriter
StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似 。
OutputStreamWriter
OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器。
try(Writer writer=new OutputStreamWriter(new FileOutputStream('readme.txt',"UTF-8"))){ }
PrintStream和PrintWriter
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法 :
- print(int)
- print(boolean)
- print(String)
- print(Object),相当于print(Object.toString())
还有一组println()方法,它会自动加上换行符 。
PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/
println()方法最终输出的是char数据。
StringWriter buffer=new StringWriter(); try(PrintWriter pw=new PrintWiter(buffer)){ pw.println("hello"); pw.println(12345); pw.println(true); }
日期和时间
Date和Calendar
Epoch Time:本质上是一个整数,同一时刻。
新旧API:
- 一套定义在 java.util 这个包里面,主要包括 Date 、 Calendar 和 TimeZone 这几个类
- 一套新的API是在Java 8引入的,定义在 java.time 这个包里面,主要包括 LocalDateTime 、 ZonedDateTime 、 ZoneId 等
Date: java.util.Date 是用于表示一个日期和时间的对象,实际上是long类型的毫秒数
public class Date implements Seializable,Cloneable,Comparable<Date>{ private transient long fastTime; }
旧API:
import java.util.Date; //旧API Date date=new Date(); //当前时间 date.getYear()+1900; //必须加上1900 date.getMonth(); //0~11 date.getDate(); //1-31 date.toGMTString(); //GMT时区 date.toLocaleString(); //本地时区
使用SimpleDateFormat对一个Date进行转换 :
- yyyy: 年
- MM: 月
- dd: 日
- HH: 小时
- mm: 分钟
- ss: 秒
var sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.format(date); //格式化时间和日期
Calendar
Calendar c=Calender.getInstance(); //获取当前时间 c.get(Calendar.YEAR); c.get(Calendar.MONTH); //0~11 c.get(Calendar.DAY_OF_MONTH); c.get(Calendar.DAY_OF_WEEK); //1~7,周日,周一~周六 c.get(Calendar.HOUR_OF_DAY); c.get(Calendar.MINUTE); c.get(Calendar.SECOND); c.get(Calendar.MILLISECOND); //设置新时间和日期 c.clear(); //清除所有 c.set(Calendar.YEAR,2019); //设置年份 ... System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getTime())); //将Calender对象扎转换为Date对象,并进行格式转换
Calendar和Date相比,其提供时区转换功能:
TimeZone tzDefault=TimeZone.getDefault(); //当前时区 TimeZone tzGMT9 = TimeZone.getTimeZone("GMT+09:00"); // GMT+9:00时区 TimeZone tzNY = TimeZone.getTimeZone("America/New_York"); // 纽约时区 tzDefault.getID(); //Asia/Shanghai 获取时区的唯一标识 TimeZone.getAvailableIDs(); //查看系统支持的ID //时区转换 c.clear(); c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
日期的加减:
c.add(Calendar.DAY_OF_MONTH,5); //也可以为负值
System.currentTimeMillis()获取当前时间戳 ,Epoch Time。
LocalDateTime
java.time 包提供了新的日期和时间API,主要涉及的类型有:
- 本地日期和时间: LocalDateTime , LocalDate , LocalTime ;
- 带时区的日期和时间: ZonedDateTime ;
- 时刻: Instant ;
- 时区: ZoneId , ZoneOffset ;
- 时间间隔: Duration 。
LocalDate d=LocalDate.now(); //当前日期 LocalTime t=LocalTime.now(); //当前时间 LocalDateTime dt=LocalDateTime.now(); //当前日期和时间 System.out.println(d); //严格按照ISO 8601格式打印 //指定日期和时间 LocalDate d2=LocalDate.of(2019,11,30); LocalTime t2=LocalTime.of(15,16,17); LocalDateTime dt2=LocalDateTime.of(2019,11,30,15,16,17); LocalDateTime dt3=LocalDateTime.of(d2,t2); //字符串转换 LocalDateTime dt=LocalDateTime.parse("2019-11-19T15:16:17"); LocalDate d=LocalDate.parse("2019-11-19"); LocalTime t=LocalTime.parse("15:16:17");
标准格式:
- 日期:yyyy-MM-dd
- 时间:HH:mm:ss
- 带毫秒的时间:HH:mm:ss.SSS
- 日期和时间:yyyy-MM-dd'T'HH:mm:ss
- 带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
DateTimeFormatter
DateTimeFormatter dtf=DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); System.out.println(dtf.format(LocalDateTime.now())); LocalDateTime dt2=LocalDateTime.parse("2019/11/30 12:59:29");
对日期进行加减:
LocalDateTime dt=LocalDateTime.of(2019,10,26,20,59,32); LocalDateTime dt2=dt.plusDay(5).minusHours(3); //链式调用
日期的调整:
- 调整年:withYear()
- 调整月:withMonth()
- 调整日:withDayOfMonth()
- 调整时:withHour()
- 调整分:withMinute()
- 调整秒:withSecond()
日期的加减,调整会自动调整不合适的日期。
使用with做更复杂的运算:
import java.time.*; import java.time.temporal.*; LocalDateTime firstDay = LocalDate.now().withDayOfMonth(1).atStartOfDay(); LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()); LocalDate nextMonthFirstDay = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
要判断两个 LocalDateTime 的先后,可以使用 isBefore() 、 isAfter() 方法,对于 LocalDate 和 LocalTime 类似。
注意到 LocalDateTime 无法与时间戳进行转换,因为 LocalDateTime 没有时区,无法确定某一时刻。
Duration 和Period
LocalDateTime start=LocalDateTime.of(..); LocalDateTime end=LocalDateTime.of(...); Duration d=Duration.between(start,end); System.out.println(d); // PT1235H10M30S Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9)); System.out.println(p); // P1M21D
ZonedDateTime
ZonedDateTime zbj=ZonedDateTime.now() ; //默认区 ZonedDateTime zny=ZonedDateTime.now(ZonedId.of("America/New_York")); //附加ZoneId LocalDateTime ldt=LocalDateTime.of(2019,9,15,20,21,23); ZoneDateTime zbj=ldt.atZone(ZoneId.systemDefault()); ZoneDateTime zny=ldt.atZone(ZoneId.of("America/New_York"));
时区转换:
ZoneDateTime zbj=ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); ZoneDateTime zny=zbj.withSameInstant(ZoneId.of("America/New_York"));
DateTimeFormatter
和 SimpleDateFormat 不同的是, DateTimeFormatter 不但是不变对象,它还是线程安全的
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, yyyy-MMMM-dd HH:mm", Locale.US); //指定Locale
Instant
public final class Instant implements ...{ private final long seconds; private final int nanos; //纳秒 }
Instant now=Instant.now(); System,out.println(now.getEpochSecond()); //toEpochMilli毫秒 Instant ins = Instant.ofEpochSecond(1568568760); ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
数据库中日期和时间
数据库 | 对应Java类 | 对应Java类(新) |
---|---|---|
DATETIME | java.util.Date | LocalDateTime |
DATE | java.util.Date | LocalDate |
TIME | java.util.Time | LocalTime |
TIMESTAMP | java.util.Timestamp | LocalDateTime |
static String timestampToString(long epochMilli, Locale lo, String zoneId){ Instant ins=Instant.of(epochMilli); DateTimeFormatter f=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM,FormatStyle.SHORT); return f.withLocale(lo).format(ZoneDateTime.ofInstant(ins,ZoneId.of(ZoneId))); }
单元测试
编写JUnit测试
单元测试就是针对最小的功能单元编写测试代码
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; public class FactorialTest{ @Test void testFact(){ assertEqual(1,Factorial.fact(1)); .... } }
对于浮点数比较:
assertEquals(0.1, Math.abs(1 - 9 / 10.0), 0.0000001); //最后为误差
使用Fixture
JUnit提供了编写测试前准备、测试后清理的固定代码,我们称之为Fixture。
编写Fixture的套路如下:
- 对于实例变量,在 @BeforeEach 中初始化,在 @AfterEach 中清理,它们在各个 @Test 方法中互不影响,因为是不同的实例;
- 对于静态变量,在 @BeforeAll 中初始化,在 @AfterAll 中清理,它们在各个 @Test 方法中均是唯一实例,会影响各个 @Test 方 法。
大多数情况下,使用 @BeforeEach 和 @AfterEach 就足够了。只有某些测试资源初始化耗费时间太长,以至于我们不得不尽量“复用”时才 会用到 @BeforeAll 和 @AfterAll 。
异常测试
@Test void testNegative(){ assertThrows(IllegalArgumentException.class,()->{ Factorial.fact(-1); //函数式接口 }) }
条件测试
@Test @EnabledOnOs(OS.WINDOWS) void testWindows() { assertEquals("C:\\test.ini", config.getConfigFile("test.ini")); } @Test @EnabledOnOs({ OS.LINUX, OS.MAC }) void testLinuxAndMac() { assertEquals("/usr/local/test.cfg", config.getConfigFile("test.cfg")); }
//还有以下注解 @Disable @EnableOnOs @DisableOnOs @DisableOnJre @EnabledIfSystemProperty(named='os.arch',matches=",*64.*") @EnabledIfEnvironmentVariable(named="DEBUG",matches="true") @EnabledIf("java.time.LocalDate.now().getDayOfWeek()==java.time.DayOfWeek.SUNDAY")
参数化测试
@ParameterizedTest @ValueSource(ints={0,1,5,100}) void testAbs(int x){ assertEquals(x,Math.abs(x)); }
传入测试参数的方法:
@ParameterizedTest @CsvSource({"abc,Abc","APPLE,Apple","gooD,Good"}) void testCapitalize(String input, String result){ assertEquals(result,StringUtils.capitalize(input)); }
可以编写一个同名静态方法来提供测试参数
@ParameterizedTest @MethodSource void testCapitalize(String input, String result){ assertEquals(result,StringUtils.capitalize(input)); } static List<Arguments> testCapitalize(){ return List.of( Arguments.arguments("abc", "Abc"), // Arguments.arguments("APPLE", "Apple"), // Arguments.arguments("gooD", "Good")); }
传入参数很多时:
@ParameterizedTest @CsvFileSource(resourse={"/test-capitialize.csv"})
JUnit只在classpath中查找指定的CSV文件,因此, test-capitalize.csv 这个文件要放到 test 目录下:
apple, Apple HELLO, Hello JUnit, Junit reSource, Resource
正则表达式
正则表达式简介
Java字符串用 \ \表示 \。
String regex="20\\d\\d"; System.out.println("2019".matches(regex));
匹配规则
含义 | 举例 | |
---|---|---|
. | 匹配一个任意字符 | "a.c"可以匹配"abc"等 |
\d | 匹配数字 | |
\w | 匹配一个字母、数字或下划线 | |
\s | 匹配空格字符 | |
\D | 匹配非数字 | |
* | 匹配任意个字符 | |
+ | 匹配至少一个字符 | |
? | 匹配0个或一个 | |
^ | 开头 | |
$ | 结尾 | |
{m,n} | m~n位字符 | |
[1-9] | 匹配指定范围 | [^1-9]非1-9 |
| | 或规则匹配 |
分组匹配
Pattern p=Pattern.complie("(\\d{3,4})\\-(\\d{7,8})"); Matcher m=p.matcher("010-12345678"); if(m.mathes()){ String g1=m.group(1); String g2=m.group(2); }
使用 Matcher 时,必须首先调用 matches() 判断是否匹配成功,匹配成功后,才能调用 group() 提取子串。
非贪婪匹配
Pattern pattern=Pattern.compile("(\\d+?)(0*)"); Matcher matcher=pattern.matcher("1230000"); if(mather.matches()){ ... }
搜索和替换
分割字符串
"a,b ;; c".split("[\\,\\;\\s+]"); //{"a","b","c"}
搜索字符串
String s = "the quick brown fox jumps over the lazy dog."; Pattern p=Pattern.compile("\\wo\\w"); Matcher m=p.matcher(s); if(m.find()){ String sub=s.substring(m.start(),m.end()); }
==获取到 Matcher 对象后,不需要调用 matches() 方法(因为匹配整个串肯定返回false),而是反复调用 find() 方法,在整个串中 搜索能匹配上 \wo\w 规则的子串,并打印出来。==
替换字符串
String r=s.replaceAll("\\s+"," ");
==反复引用==
String r = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");
用匹配的分组子串 ([a-z]{4}) 替 换了 $1 。
加密与安全
编码算法
URL编码
- 如果字符是 A ~ Z , a ~ z , 0 ~ 9 以及 - 、 _ 、 . 、 * ,则保持不变;
- 如果是其他字符,先转换为UTF-8编码,然后对每个字节以 %XX 表示
String encode=URLEncoder.encode("中文",StandardCharsets.UTF_8); String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", StandardCharsets.UTF_8);
Base64编码
对二进制进行编码,表示为文本格式,对3字节的二进制按6bit一组,用4个int表示然后查表,把int整数用索引对应到字符,得到编码后的字符串。
因为6位整数的范围总是 0 ~ 63 ,所以,能用64个字符表示:字符 A ~ Z 对应索引 0 ~ 25 ,字符 a ~ z 对应索引 26 ~ 51 ,字 符 0 ~ 9 对应索引 52 ~ 61 ,最后两个索引 62 、 63 分别用字符 + 和 / 表示。
byte[] in=new byte[]{] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad }; } String b64encoded=Base64.getEncoder().encodeToString(in); //解码 byte[] out=Base64.getDecoder().decode("5Lit");
如果输入的 byte[] 数组长度不是3的整数倍:这种情况下,需要对输入的末尾补一个或两个 0x00 ,编码后,在结 尾加一个 = 表示补充了1个 0x00 ,加两个 = 表示补充了2个 0x00 ,解码的时候,去掉末尾补充的一个或两个 0x00 即可
因为标准的Base64编码会出现 + 、 / 和 = ,所以不适合把Base64编码后的字符串放到URL中。一种针对URL的Base64编码可以在URL 中使用的Base64编码,它仅仅是把 + 变成 - , / 变成 _ 。
String b64encoded=Base.getUrlEncoder().encodeToString(in);
哈希算法
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
- 相同的输入一定得到相同的输出
- 不同的输入大概路得到不同的输出
哈希算法的目的就是为了验证原始数据是否被篡改。
"hello".hashCode(); // 0x5e918d2 四字节整数
常用hash算法:
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
MD5 | 128 | 16 |
SHA-1 | 160 | 20 |
RipeMD-160 | 160 | 20 |
SHA-256 | 256 | 32 |
SHA-512 | 512 | 64 |
MessageDigest md=MessageDigest.getInstance("MD5"); md.update("Hello, world".getBytes("UTF-8")); byte[] result=md.digest(); //16bytes System.out.println(new BigInteger(1,result).toString(16));
BouncyCastle
//注册BouncyCastle Security.addProvider(new BouncyCastleProvider()); //按正常名称调用 MessageDigest md=MessageDigest.getInstance("RipeMD160");
Hmac算法
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算 法。
HmacMD5 ≈ md5(secure_random_key, input)
import javax.crypto.*; import java.math.BigInteger; KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5"); SecretKey key=keyGen.generateKey(); byte[] skey=key.getEncoded(); System.out.println(new Integer(1,skey).toString(16)); Mac mac=Mac.getInstance("HmacMD5"); mac.init(key); mac.update("HelloWorld".getBytes("UTF-8")); byte[] result=mac.doFinal(); System.out.println(new BigInteger(1,result).toString(16));
验证
import javax.crypto.*; import javax.crypto.spec.*; import java.util.Arrays; byte[] hkey = new byte[] { 106, 70, -110, 125, 39, -20, 52, 56, 85, 9, -19, -72, 52, -53, 52, -45, -6, 119, -63, 30, 20, -83, -28, 77, 98, 109, -32, -76, 121, -106, 0, -74, -107, -114, -45, 104, 104, -8, 2, 121, 6, 97, -18, -13, -63, -30, -125, -103, -80, -46, 113, -14, 68, 32, -46, 101, -116, 104, -81, -108, 122, 89, -106, -109 }; SecretKey key=new SecretKeySpec(hkey,"HmacMD5"); Mac mac=Mac.getInstance("HmacMD5"); mac.init(key); mac.update("HelloWorld".getBytes("UTF-8")); bytes[] result=mac.doFinal(); System.out.println(Arrays.toString(result)); // [126, 59, 37, 63, 73, 90, 111, -96, -77, 15, 82, -74, 122, -55, -67, 54]
对称加密算法
算法 | 秘钥长度 | 工作模式 | 填充模式 |
---|---|---|---|
DES | 56/64 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/... |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/... |
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java标准库提供的算法实现并不包括 所有的工作模式和所有填充模式,但是通常我们只需要挑选常用的使用就可以了。
AES加密
ECB模式加密
import java.security.*; import java.util.Base64; import javax.crypto.*; import javax.crypto.spec.*; public class Solution{ public static void main(String[] args) throws GeneralSecurityException { String message = "Hello, world!"; System.out.println("Message: " + message); // 128位密钥 = 16 bytes Key: byte[] key = "1234567890abcdef".getBytes(StandardCharsets.UTF_8); // 加密: byte[] data = message.getBytes(StandardCharsets.UTF_8); byte[] encrypted = encrypt(key, data); System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted)); // 解密: byte[] decrypted = decrypt(key, encrypted); System.out.println("Decrypted: " + new String(decrypted, StandardCharsets.UTF_8)); } public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKey keySpec = new SecretKeySpec(key, "AES"); cipher.init(Cipher.ENCRYPT_MODE,keySpec); return cipher.doFinal(input); } public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException{ Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKey keySpec=new SecretKeySpec(key,"AES"); cipher.init(Cipher.DECRYPT_MODE,keySpec); return cipher.doFinal(input); } }
ECB模式是最简单的AES加密模式,它只需要一个固定长度的密钥,固定的明文会生成固定的密文,这种一对一的加密方式会导致安全性 降低,更好的方式是通过CBC模式,它需要一个随机数作为IV参数,这样对于同一份明文,每次生成的密文都不同:
import java.nio.charset.StandardCharsets; import java.security.*; import java.util.Base64; import javax.crypto.*; import javax.crypto.spec.*; public class Solution{ public static void main(String[] args) throws GeneralSecurityException { String message = "Hello, world!"; System.out.println("Message: " + message); // 256位密钥 = 32 bytes Key: byte[] key = "1234567890abcdef1234567890abcdef".getBytes(StandardCharsets.UTF_8); ; // 加密: byte[] data = message.getBytes(StandardCharsets.UTF_8); byte[] encrypted = encrypt(key, data); System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted)); // 解密: byte[] decrypted = decrypt(key, encrypted); System.out.println("Decrypted: " + new String(decrypted, StandardCharsets.UTF_8)); } public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey keySpec = new SecretKeySpec(key, "AES"); // CBC模式需要生成一个16 bytes的initialization vector: SecureRandom sr=SecureRandom.getInstanceStrong(); byte[] iv=sr.generateSeed(16); IvParameterSpec ivps=new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE,keySpec,ivps); // IV不需要保密,把IV和密文一起返回: byte[] data=cipher.doFinal(input); return join(iv,data); } public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException{ //将input分割成IV和密文 byte[] iv=new byte[16]; byte[] data=new byte[input.length-16]; System.arraycopy(input,0,iv,0,16); System.arraycopy(input,16,data,0,data.length); //解密 Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey keySpec=new SecretKeySpec(key,"AES"); IvParameterSpec ivps=new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE,keySpec,ivps); return cipher.doFinal(data); } public static byte[] join(byte[] bs1, byte[] bs2){ byte[] r=new byte[bs1.length+bs2.length]; System.arraycopy(bs1, 0, r, 0, bs1.length); System.arraycopy(bs2, 0, r, bs1.length, bs2.length); return r; } }
口令加密方式
用户输入的口令,通常还需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进 行加密。
key = generate(userPassword, secureRandomPassword);
import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.Base64; import javax.crypto.*; import javax.crypto.spec.*; public class Solution{ public static void main(String[] args) throws GeneralSecurityException { Security.addProvider(new BouncyCastleProvider()); String message = "Hello, world!"; System.out.println("Message: " + message); // 加 密 口 令 : String password = "hello12345"; //16bytes随机Salt byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16); System.out.printf("salt: %032x\n", new BigInteger(1, salt)); // 加密: byte[] data = message.getBytes(StandardCharsets.UTF_8); byte[] encrypted = encrypt(password,salt, data); System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted)); // 解密: byte[] decrypted = decrypt(password,salt, encrypted); System.out.println("Decrypted: " + new String(decrypted, StandardCharsets.UTF_8)); } public static byte[] encrypt(String password,byte[] salt, byte[] input) throws GeneralSecurityException { PBEKeySpec keySpec=new PBEKeySpec(password.toCharArray()); SecretKeyFactory secretKeyFactory=SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); SecretKey secretKey=secretKeyFactory.generateSecret(keySpec); PBEParameterSpec pbeps=new PBEParameterSpec(salt,1000); Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); cipher.init(Cipher.ENCRYPT_MODE,secretKey,pbeps); return cipher.doFinal(input); } public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException{ PBEKeySpec keySpec=new PBEKeySpec(password.toCharArray()); SecretKeyFactory secretKeyFactory=SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); SecretKey secretKey=secretKeyFactory.generateSecret(keySpec); PBEParameterSpec pbeps=new PBEParameterSpec(salt,1000); Cipher cipher=Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC"); cipher.init(Cipher.DECRYPT_MODE,secretKey,pbeps); return cipher.doFinal(input); } }
秘钥交换算法
密钥交换算法即DH算法:Diffie-Hellman算法,
假设甲乙双方需要传递密钥,他们之间可以这么做:
- 甲首选选择一个素数 p ,例如509,底数 g ,任选,例如5,随机数 a ,例如123,然后计算 A=g^a mod p ,结果是215,然后,甲 发送 p=509 , g=5 , A=215 给乙;
- 乙方收到后,也选择一个随机数 b ,例如,456,然后计算 B=g^b mod p ,结果是181,乙再同时计算 s=A^b mod p ,结果是121;
- 乙把计算的 B=181 发给甲,甲计算 s=B^a mod p 的余数,计算结果与乙算出的结果一样,都是121。
所以最终双方协商出的密钥 s 是121。注意到这个密钥 s 并没有在网络上传输。而通过网络传输的 p , g , A 和 B 是无法推算 出 s 的,因为实际算法选择的素数是非常大的。
如果我们把 a 看成甲的私钥, A 看成甲的公钥, b 看成乙的私钥, B 看成乙的公钥,DH算法的本质就是双方各自生成自己的私钥和公 钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥 secretKey ,DH算法通过数学定律保证了双 方各自计算出的 secretKey 是相同的。
DH算法是一种密钥交换协议,通信双方通过不安全的信道协商密钥,然后进行对称加密传输。
非对称加密算法
非对称加密就是加密和解密使用的不是相同的密钥:只有同一个公钥-私钥对才能正常加解密。
非对称加密相比对称加密的显著优点在于,对称加密需要协商密钥,而非对称加密可以安全地公开各自的公钥,在N个人之间通信的时 候:使用非对称加密只需要N个密钥对,每个人只管理自己的密钥对。而使用对称加密需要则需要 N*(N-1)/2 个密钥,因此每个人需要管 理 N-1 个密钥,密钥管理难度大,而且非常容易泄漏。因为非对称加密的缺点就是运算速度非常慢,比对 称加密要慢很多。
在实际应用的时候,非对称加密总是和对称加密一起使用。假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥, 然后:
- 小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
- 小红用自己的RSA私钥解密得到AES口令;
- 双方使用这个共享的AES口令用AES加密通信。
class Person{ String name; PrivateKey sk; PublicKey pk; //公钥 public Person(String name) throws GeneralSecurityException{ this.name=name; KeyPairGenerator kpGen=KeyPairGenerator.getInstance("RSA"); kpGen.initialize(1024); KeyPair kp=kpGen.generateKeyPair(); this.sk=kp.getPrivate(); this.pk=kp.getPublic(); } //将私钥导出为字节 public byte[] getPrivateKey(){ return this.sk.getEncoded(); } //将公钥导出为字节 public byte[] getPublicKey(){ return this.pk.getEncoded(); } //用公钥加密 public byte[] encrypt(byte[] message) throws GeneralSecurityException{ Cipher cipher=Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE,this.pk); return cipher.doFinal(input); } //用私钥解密 public byte[] decrypt(byte[] input) throws GeneralSecurityException{ Cipher cipher=Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE,this.pk); return cipher.doFinal(input); } } import java.math.BigInteger; import java.security.*; import javax.crypto.Cipher; public class Main{ public static void main(String[] args) throws Exception{ //明文 byte[] plain="Hello, RSA".getBytes("UTF-8"); //创建公钥/私钥 Person Alice=new Person("Alice"); byte[] pk=Alice.getPublicKey(); byte[] encrypted=Alice.encrypt(plain); System.out.println(String.format("encrypted:%x",new Integer(1,encrypted))); byte[] sk=Alice.getPrivateKey(); byte[] decrypted=Alice.decrypt(encrypted); } }
以RSA算法为例,它的密钥有256/512/1024/2048/4096等不同的长度。长度越长,密码强度越大,当然计算速度也越慢。
如果修改待加密的 byte[] 数据的大小,可以发现,使用512bit的RSA加密时,明文长度不能超过53字节,使用1024bit的RSA加密时,明 文长度不能超过117字节,这也是为什么使用RSA的时候,总是配合AES一起使用,即用AES加密任意长度的明文,用RSA加密AES口 令。
只使用非对称加密算法不能防止中间人攻击。
签名算法
私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。使用数字签名的目 的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖。
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名,即:
signature = encrypt(privateKey, sha256(message));
对签名进行验证实际上就是用公钥解密:
hash=decrypt(publicKey,signature);
然后把解密后的哈希与原始消息的哈希进行对比。
常用的数字签名算法:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*; public class Main{ public static void main(String[] args) throws GeneralSecurityException{ KeyPairGenerator kpGen=KeyPairGenerator.getInstance("RSA"); kpGen.initialize(1024); KeyPair kp=kpGen.generateKeyPair(); PrivateKey sk=kp.getPrivate(); PublicKey pk=kp.getPublic(); //代签名的消息 byte[] message="Hello, I am Bob!".getBytes(StandardCharsets.UTF_8); //用私钥签名 Signature s=Signature.getInstance("SHA1withRSA"); s.initSign(sk); s.update(message); byte[] signed=s.sign(); System.out.println(String.format("signature: %x",new BigInteger(1,signed))); //用公钥验证 Signature v=Signature.getInstance("SHA1withRSA"); v.initVerify(pk); v.update(message); boolean valid=v.verify(signed); System.out.println("valid?"+valid); } static KeyStore loadKeyStore(String keyStoreFile, String password) }
DSA签名
DSA只能配合SHA使用,常用算法为:
- SHA1withDSA
- SHA256withDSA
- SHA512withDSA
ECDSA签名
椭圆曲线签名算法ECDSA:Elliptic Curve Digital Signature Algorithm也是一种常用的签名算法,它的特点是可以从私钥推出公钥。比特 币的签名算法就采用了ECDSA算法,使用标准椭圆曲线secp256k1。BouncyCastle提供了ECDSA的完整实现。
数字签名用于:防止伪造、防止抵赖、检测篡改。
数字证书
数字证书就是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
数字证书可以防止中间人攻击,因为它采用==链式签名认证==,即通过根证书(Root CA)去签名下一级证书,这样层层签名,直到最终的用 户证书。而Root CA证书内置于操作系统中,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造的。
在Java程序中,数字证书存储在一种Java专用的key store文件中,JDK提供了一系列命令来创建和管理key store。我们用下面的命令创 建一个key store,并设定口令123456:
keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 3650 -alias mycert keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN"
几个主要的参数是:
- keyalg:指定RSA加密算法;
- sigalg:指定SHA1withRSA签名算法;
- validity:指定证书有效期;
- alias:指定证书在程序中引用的名称;
- dname:最重要的 CN=www.sample.com 指定了 Common Name ,如果证书用在HTTPS中,这个名称必须与域名完全一。
执行上述命令,JDK会在当前目录创建一个 my.keystore 文件,并存储创建成功的一个私钥和一个证书,它的别名是 mycert 。
import java.io.InputStream; import java.math.BigInteger; import java.security.*; import java.security.cert.*; import javax.crypto.Cipher; public class Main{ public static void main(String[] args) throws Exception{ byte[] message="Hello, use X.509 cert!".getBytes("UTF-8"); //读取KeyStore KeyStore ks=loadKeyStore("/my.keystore","123456"); //读取私钥 PrivateKey privateKey=(PrivateKey)ks.getKey("mycert","123456".toCharArray()); //读取证书 X509Certificate certificate=(X509Certificate)ks.getCertificate("mycert"); //加密 byte[] encrypted=encrypt(certificate,message); System.out.println(String.format("encrypted: %x",new BigInteger(1,encrypted))); //解密 byte[] sign=sign(privateKey,encrypted); System.out.println("decrypted:"+new String(decrypted,"UTF-8")); //签名 byte[] sign=sign(privateKey,certificate,message); System.out.println(String.format("signature: %x", new BigInteger(1, sign))); //验证签名 boolean verified=verify(certificate,message,sign); System.out.println("verify: " + verified); } static KeyStore loadKeyStore(String keyStoreFile, String password) { try(InputStream in=Main.class.getResourseAsStream(keyStoreFile)){ if(in==null){ throws new RuntimeException("file not found in classpath:"+keyStoreFile); } keyStore ks=KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(in,password.toCharArray()); return ks; }catch(Exception e){ throw new RuntimeException(e); } } static byte[] encrypt(X509Certificate certificate, byte[] message) throws GeneralSecurityException{ Cipher cipher=Cipher.getInstance(certificate.getPublicKey().getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE,certificate.getPublicKey()); return cipher.doFinal(message); } static byte[] decrypt(PrivateKey privateKey, byte[] data) throws GeneralSecurityException{ Cipher cipher=Cipher.getInstance(privateKey.getAlgorithm()); cipher,init(Cipher.DECRYPT_MODE,privateKey); return cipher.doFinal(data); } static byte[] sign(PrivateKey privateKey, X509Certificate certificate, byte[] message) throws GeneralSecurityException{ Signature signature=Signature.getInsance(certificate.getSigAlgName()); signature.initSign(privateKey); signature.update(message); return signature.sign(); } static boolean verify(X509Certificate certificate, byte[] message, byte[] sig) throws GeneralSecurityException{ Signature signature=Signature.getInstance(certificate.getSigAlgName()); signature.initVerify(certificate); signature.update(message); return signature.verify(sig); } }
从key store直接读取了私钥-公钥对,私钥以 PrivateKey 实例表示,公钥以 X509Certificate 表示,实际上数字证 书只包含公钥,因此,读取证书并不需要口令,只有读取私钥才需要。如果部署到Web服务器上,例如Nginx,需要把私钥导出为Private Key格式,把证书导出为X509Certificate格式。
以HTTPS协议为例,浏览器和服务器建立安全连接的步骤如下:
- 浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书;
- 浏览器用操作系统内置的Root CA来验证服务器的证书是否有效,如果有效,就使用该证书加密一个随机的AES口令并发送给服务 器;
- 服务器用自己的私钥解密获得AES口令,并在后续通讯中使用AES加密。
上述流程只是一种最常见的单向验证。如果服务器还要验证客户端,那么客户端也需要把自己的证书发送给服务器验证,这种场景常见于 网银等。
多线程
和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销大,尤其是在Windows系统上;
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导 致整个进程崩溃。
创建多线程
当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行 main() 方法。 在 main() 方法中,我们又可以启动其他线程。
从Thread派生,然后覆写run()方法:
class MyThread extends Thread{ @Override public void run(){ ... } }
创建Thread实例,传入Runnable实例
class MyRunnable implements Runnable{ @Override public void run(){ ... } }
使用lambda语法简写
Thread t=new Thread(()->{ .... }) t.start();
线程优先级
Thread.setPriority(int n); //1-10,默认为5
线程的状态
Java线程的状态有以下几种:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行 run() 方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行 sleep() 方法正在计时等待;
- Terminated:线程已终止,因为 run() 方法执行完毕
当线程启动后,它可以在 Runnable 、 Blocked 、 Waiting 和 Timed Waiting 这几个状态之间切换,直到最后变成 Terminated 状态, 线程终止。
线程终止的原因有:
- 线程正常终止:run()方法执行到return语句
- 线程意外终止:run()方法未捕获的一场导致线程终止
- 对某个线程Thread实例调用stop()方法强制终止
public static void main (String[] args) throws InterruptedException{ Thread t=new Thread(()->{ System.out.println("Hello"); }); t.start(); t.jion(); //等待t线程结束后在运行 }
中断线程
中断一个线程非常简单,只需要在其他线程中对目标线程调用 interrupt() 方法,目标线程需要反复检测自身状态是否是interrupted状 态,如果是,就立刻结束运行。
public class Main{ public static void main(String[] args) throws InterruptedException{ Thread t=new MyThread(); t.start(); Thread.sleep(1); //暂停1毫秒 t.interrupt(); t.join(); System.out.println("end"); } } class MyThread extends Thread{ public void run(){ int n=0; while(!isInterrupted()){ n++; System.out.println(n+" hello!"); } } }
另一个常用的中断线程的方法是设置标志位。我们通常会用一个 running 标志位来标识线程是否应该继续运行,在外部线程中,通过 把 HelloThread.running 置为 false ,就可以让线程结束:
public class Main{ public static void main(String[] args) throws InterruptedException{ HelloThread t =new Thread(); t.start(); Thread.sleep(1); t.running=false; //标志位false } } class HelloThread extends Thread{ public volatile boolean running=true; public void run(){ int n=0; while(running){ n++; System.out.println(n+" Hello!"); } } System.out.println("end!"); }
线程间共享变量需要使用 volatile 关键字标记,确保每个 线程都能读取到更新后的变量值。
volatile 关键字的目的是告诉虚拟机:每次访问变量时,总是获取主内存的最新值; 每次修改变量后,立刻回写到主内存。
守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束。
Thread t=new MyThread(); t.setDaemon(true); t.start();
线程同步
这种加 锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
class Counter{ public static final Object lock=new Object(); public static int count; } class AddThread extends Thread{ public void run(){ for(int i=0;i<1000;i++){ synchronized(Counter.lock){ Counter.count--; } } } } class DecThread extends Thread{ public void run(){ for(int i=0;i<1000;i++){ synchronized(Counter.lock){ Counter.count-=1; } } } }
在使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized 结束处正确释放锁
public void add(int m){ synchronized(obj){ if(m<0)throw new RuntimeException(); this.val+=m; } // 无 论 有 无 异 常 , 都 会 在 此 释 放 锁 }
不需要synchronized的操作
JVM规范定义了几种原子操作:
- 基本类型( long 和 double 除外)赋值,例如: int n = m ;
- 引用类型赋值,例如: List< String> list = anotherList 。
long 和 double 是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把 long 和 double 的赋值 作为原子操作实现的。
单条原子操作的语句不需要同步。但是,如果是多行赋值语句,就必须保证是同步操作,例如:
class Pair{ int first; int last; public void set(int first, int last){ synchronized(this){ this.first=first; this.last=last; } } }
也可以通过转换,将非原子操作变为原子操作:
int[] pair; int[] ps=new int[]{first,last}; this.pair=ps;
这里的 ps 是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。
同步方法
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)。Java标准库的 java.lang.StringBuffer 也是线程安全的,还有一些不变类,例如 String , Integer , LocalDate ,它们的所有成员变量都是 final ,多线程同时访问时只能读不能写,这些不 变类也是线程安全的。最后,类似 Math 这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如 ArrayList ,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只 读取,不写入,那么 ArrayList 是可以安全地在线程间共享的。 没有特殊说明时,一个类默认是非线程安全的。
public void add(int n){ synchronized(this){ count+=n; } } //等价 public synchronized void add(int n){ }
==用 synchronized 修饰的方法就是同步方法,它表示整个方法都必须用 this 实例加锁。==
对于静态方法,锁住的是该类的Class实例:
public synchronized static void test(int n){ }
死锁
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录 +1,每退出 synchronized 块,记录-1,减到0的时候,才会真正释放锁。
public class Counter{ private int count=0; public synchronized void add(int n){ if(n<0){ dec(-n); }else{ count+=n; } } public synchronized void dec(int n){ count+=n; } }
如何避免死锁呢?答案是:线程获取锁的顺序要一致。
使用wait和notify
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
class TaskQueue{ Queue<String> queue=new LinkedList<>(); public synchronized void addTask(String s){ this.queue.add(s); this.notify(); //唤醒在this锁等待的线程 } public synchronized String getTask(){ while(queue.isEmpty()){ //释放this锁 this.wait(); //获得this锁 } return queue.remove(); } }
wait() 方法必须在当前获取的锁对象上调用,这里获取的是 this 锁,因此调用 this.wait() 。
调用 wait() 方法后,线程进入等待状态, wait() 方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后, wait() 方 法才会返回,然后,继续执行下一条语句。
即使线程在 getTask() 内部等待,其他线程如果拿不到 this 锁,照样无法执行 addTask() ?
这个问题的关键就在于 wait() 方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在 Object 类的一个 native 方 法,也就是由JVM的C代码实现的。其次,必须在 synchronized 块中才能调用 wait() 方法,因为 wait() 方法调用时,会释放线程获得 的锁, wait() 方法返回后,线程又会重新试图获得锁。
注意到在往队列中添加了任务后,线程立刻对 this 锁对象调用 notify() 方法,这个方***唤醒一个正在 this 锁等待的线程(就是 在 getTask() 中位于 this.wait() 的线程),从而使得等待线程从 this.wait() 方法返回。
使用 notifyAll() 将唤醒所有当 前正在 this 锁等待的线程,而 notify() 只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正 在 getTask() 方法内部的 wait() 中等待,使用 notifyAll() 将一次性全部唤醒。通常来说, notifyAll() 更安全。有些时候,如果我 们的代码逻辑考虑不周,用 notify() 会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
使用ReentrantLock
java.util.concurrent.locks 包提供的 ReentrantLock 用于替代 synchronized 加锁
public class Counter{ private final Lock lock=new ReentrantLock(); private int count; public void add(int n){ lock,lock(); try{ count+=n; }finally{ lock.unlock(); } } }
ReentrantLock 是可重入锁,它和 synchronized 一样,一个线程可以多次获取同一个锁。和 synchronized 不同的是, ReentrantLock 可以尝试获取锁:
if(lock.tryLock(1,TimeUnit.SECONDS)){ try{ }finally{ lock.ulock(); } }
使用 ReentrantLock 比直接使用 synchronized 更安全,线程在 tryLock() 失败的时候不会导致死锁。
使用Condition
class TaskQueue{ Queue<String> queue=new LinkedList<>(); private final Lock lock=new ReentantLock(); private final Condition condition=lock.newCondition(); public void addTask(String s){ lock.lock(); try{ queue.add(s); condition.signalAll(); }finally{ lock.unlock(); } } public String getTask(){ lock.lock(); try{ while(queue.isEmpty()){ condition.await(); } return queue.remove(); }finally{ lock.unlock(); } }
if(condition.await(1,TimeUnit.SECOND)){ }else{ }
使用ReadWriteLock
使用 ReadWriteLock 可以解决这个问题,它保证:
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。
public class Counter{ private final ReadWriterLock rwLock=new ReentrantReadWriterLock(); private final Lock rlock=rwLock.readLock(); private final Lock wlock=rwLock.WriterLock(); private int[] counts=new int[10]; public void inc(int index){ wlock.lock(); try{ counts[index]++; }finally{ wlock.unlock(); } } public int[] get(){ rlock.lock(); try{ return Arrays.copyOf(counts,counts.length); }finally{ rlock.unlock(); } } }
使用 ReadWriteLock 时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。
使用StampedLock
ReadWriteLock ,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即 读的过程中不允许写,这是一种悲观的读锁。
StampedLock 和 ReadWriteLock 相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所 以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
==乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写 入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行==。
public class Point{ private final StampedLock stampedLock=new StampedLock(); private double x; private double y; public void move(double deltaX, double deltaY){ long stamp=stampedLock.writeLock(); //获取写锁 try{ x+=deltaX; y+=deltaY; }finally{ stampedLock.unlockWriter(stamp); } } public double distanceFromOrigin(){ long stamp=stampedLock.tryOptimisticRead(); //乐观读锁 //以下两行代码不是原子操作 double currentX=x; double currentY=y; if(!stampedLock.validate(stamp)){ //检查乐观锁后是否有其他写锁发生 stamp=stampedLock.readLock(); //获取一个悲观锁 try{ currentX=x; currentY=y; }finally{ stampedLock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } }
StampedLock 把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二 是 StampedLock 是不可重入锁,不能在一个线程中反复获取同一个锁。
使用Concurrent集合
interface | non-thread-safe | thread-safe |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurretHashMap |
Set | HashSet/TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque/LinkedList | ArrayBlockingQueue/LinkedBlockingQueue |
Deque | ArrrayDeque/LinkedList | LinkedBlockingDeque |
Map<String,String> map=ConcurrentHashMap<>(); //在不同线程读写 map.put("A","1");
线程安全集合转换器
Map unsafeMap=new HashMap(); Map threadSafeMap=Collections.synchronizedMap(unsafeMap);
使用Atomic
Java的 java.util.concurrent 包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位 于 java.util.concurrent.atomic 包。
以 AtomicInteger 为例,它提供的主要操作有:
- 增加值并返回新值: int addAndGet(int delta)
- 加1后返回新值: int incrementAndGet()
- 获取当前值: int get()
- 用CAS方式设置: int compareAndSet(int expect, int update)
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
public int increamentAndGet(AtomicInteger var){ int prev,next; do{ prev=var.get(); next=prev+1; }while(!var.compareAndSet(prev,next)); return prev; }
class IDGenerator{ AtomicLong var=new AtomicLong(0); public long getNextId(){ return var.incrementAndGet(); } }
使用线程池
线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果 所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
Java标准库提供了 ExecutorService 接口表示线程池,它的典型用法如下:
ExecutorService executor=Executors.newFixedThreadPool(3); //提交任务 executor.submit(task1);
常用的实现还有:
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
class Task implements Runnable{ } //创建一个固定大小的线程池 ExecutorService es=Executors.newFixedThreadPool(4); for(int i=0;i<6;i++){ es.submit(new Task(" "+i)); } es.shutdown(); //关闭线程池
使用 shutdown() 方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关 闭。 shutdownNow() 会立刻停止正在执行的任务, awaitTermination() 则会等待指定的时间让线程池关闭。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); } //创建指定动态范围的线程池 int min=4,max=10; ExecutorService es=new ThreadPoolExecutor(min,max,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
放入 ScheduledThreadPool 的任务可以定期反复执行。
ScheduledExecutorService ses=Executors.newScheduledThreadPool(4); ses.schedule(new Task("one-time"),1,TimeUnit.SECONDS); //1秒后执行一次 // 2 秒 后 开 始 执 行 定 时 任 务 , 每 3 秒 执 行 : ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS); // 3 秒 后 开 始 执 行 定 时 任 务 , 以 3 秒 为 间 隔 执 行 : ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间;而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务。
使用Future
Runnable 接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不 便。所以,Java标准库还提供了一个 Callable 接口,和 Runnable 接口比,它多了一个返回值:
class Task implements Callable<String>{ public String call() throws Exception{ return ...; } }
Callable 接口是一个泛型接口,可以返回指定类型的结
ExecutorService.submit() 方法,可以看到,它返回了一个 Future 类型,一个 Future 类型的实例代表一个未来能获取结 果的对象:
ExecutorService es=Executors.newFixedThreadPool(4); //定义任务 Callable<String> task=new Task(); Future<String> future=es.submit(task); //从Future获取异步执行返回的结果 String result=future.get(); //可能阻塞
一个 Future< V> 接口表示一个未来可能会返回的结果,它定义的方法有:
- get() :获取结果(可能会等待)
- get(long timeout, TimeUnit unit) :获取结果,但只等待指定的时间;
- cancel(boolean mayInterruptIfRunning) :取消当前任务;
- isDone() :判断任务是否已完成
使用CompletableFuture
从Java 8开始引入了 CompletableFuture ,它针对 Future 做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回 调对象的回调方法。
import java.util.concurrent.CompletableFuture; public class Main{ public static void main(String[] args){ //创建异步执行任务 CompletableFuture<Double> cd=CompletableFuture.supplyAsync(Main::fetchPrice); //如果执行成功 cf.thenAccept((result)->{ System.out.println("price: "+result); }); //如果执行异常 cf.exceptionally((e)->{ e.printStackTrace(); return null; }); //主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭 Thread.sleep(2000); } static Double fetchPrice(){ try{ Thread.sleep(1000); }catch(InterruptException e){} if(Math.random()<0.3){ throw new RuntimeException("fetch price failed!"); } retur 5+Math.random()*20; } }
CompletableFuture 的优点是:
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行。
如果只是实现了异步回调机制,我们还看不出 CompletableFuture 相比 Future 的优势。 CompletableFuture 更强大的功能是,多 个 CompletableFuture 可以串行执行,例如,定义两个 CompletableFuture ,第一个 CompletableFuture 根据证券名称查询证券代码, 第二个 CompletableFuture 根据证券代码查询证券价格。
// 第一个任务: CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油"); }); // cfQuery成功后继续执行下一个任务: CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> { return fetchPrice(code); }); // cfFetch成功后打印结果: cfFetch.thenAccept((result) -> { System.out.println("price: " + result); }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(2000);
除了串行执行外,多个 CompletableFuture 还可以并行执行。例如,我们考虑这样的场景:同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作:
// 两个CompletableFuture执行异步查询: CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油", "https://finance.sina.com.cn/code/"); }); CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油", "https://money.163.com/code/"); }); // 用anyOf合并为一个新的CompletableFuture: CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163); // 两个CompletableFuture执行异步查询: CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> { return fetchPrice((String) code, "https://finance.sina.com.cn/price/"); }); CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> { return fetchPrice((String) code, "https://money.163.com/price/"); }); // 用anyOf合并为一个新的CompletableFuture: CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163); // 最终结果: cfFetch.thenAccept((result) -> { System.out.println("price: " + result); }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(2000);
除了 anyOf() 可以实现“任意个 CompletableFuture 只要一个成功”, allOf() 可以实现“所有 CompletableFuture 都必须成功”,这些组 合操作可以实现非常复杂的异步流程控制。
使用ForkJoin
Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。
Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反 复“裂变”成一系列小任务。
class SumTask extends RecursiveTask<Long>{ static final int THRESHOLD=500; 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(1); //放慢速度 }catch(InterruptedException e){ } } return sum; } //任务太大,一分为二 int mid=(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(); System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result); return result; } } //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();
核心代码 SumTask 继承自 RecursiveTask ,在 compute() 方法中,关键是如何“分裂”出子任务并且提交子任务;Java标准库提供的 java.util.Arrays.parallelSort(array) 可以进行并行排序,它的原理就 是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。
使用ThreadLocal
在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务 信息等。
Java标准库提供了一个特殊的 ThreadLocal ,它可以在一个线程中传递同一个对象。
static ThreadLocal<String> threadLocalUser=new ThreadLocal<>();
实际上,可以把 ThreadLocal 看成一个全局 Map<Thread, Object> :每个线程获取 ThreadLocal 变量时,总是使用 Thread 自身作为 key:
Object threadLocalValue=threadLocalMap.get(Thread.currentThread());
因此, ThreadLocal 相当于给每个线程都开辟了一个独立的存储空间,各个线程的 ThreadLocal 关联的实例互不干扰。
最后,特别注意 ThreadLocal 一定要在 finally 中清除:
try { threadLocalUser.set(user); ... } finally { threadLocalUser.remove(); }
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果 ThreadLocal 没有被清除,该线程执行其他代码时,会把上 一次的状态带进去。
为了保证能释放 ThreadLocal 关联的实例,我们可以通过 AutoCloseable 接口配合 try (resource) {...} 结构,让编译器自动为我们 关闭。例如,一个保存了当前用户名的 ThreadLocal 可以封装为一个 UserContext 对象:
public class UserContext implements AutoCloseable{ static final ThreadLocal<String> ctx=new THreadLocal<>(); public UserContext(String user){ ctx.set(user); } public static String currentUser(){ return ctx.get(); } @Override public void close(){ ctx.remove(); } } try(var ctx=new UserContext("Bob")){ // 可 任 意 调 用 UserContext.currentUser(): S tring currentUser = UserContext.currentUser(); } // 在 此 自 动 调 用 UserContext.close() 方 法 释 放 ThreadLocal 关 联 对 象
Maven基础
Maven是一个Java项目管理和构建工具,它可以定义项目结构、项目依赖,并使用统一的方式进行自动化构建,是Java项目不可缺少的 工具。
Maven介绍
Maven就是是专门为Java项目打造的管理和构建工具,它的主要功能有:
- 提供了一套标准化的项目结构;
- 提供了一套标准化的构建流程(编译,测试,打包,发布……);
- 提供了一套依赖管理机制。
项目的根目录 a-maven-project 是项目名,它有一个项目描述文件 pom.xml ,存放Java源码的目录是 src/main/java ,存放资源文件的 目录是 src/main/resources ,存放测试源码的目录是 src/test/java ,存放测试资源的目录是 src/test/resources ,最后,所有编 译、打包生成的文件都放在 target 目录里。这些就是一个Maven项目的标准目录结构。
在pom.xml中,groupId 类似于Java的包名,通常是公司或组织名称, artifactId 类似于Java的类名,通常是项目名称,再加上 version ,一 个Maven工程就是由 groupId , artifactId 和 version 作为唯一标识。
<dependencies> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependencies>
使用 < dependency> 声明一个依赖后,Maven就会自动下载这个依赖包并把它放到classpath中。
安装Maven
M2_HOME=/path/to/maven-3.6.x PATH=$PATH:$M2_HOME/bin
Windows上可以将%M2_HOME\bin添加到系统Path变量中。
mvn -version
依赖管理
Maven的第一个作用就是解决依赖管理。我们声明了自己的项目需要 abc ,Maven会自动导入 abc 的jar包,再判断出 abc 需 要 xyz ,又会自动导入 xyz 的jar包,这样,最终我们的项目会依赖 abc 和 xyz 两个jar包。
依赖关系
scope | 说明 | 示例 |
---|---|---|
compile | 编译时需要用到jar包 | commons-logging |
test | 编译Test时需要用到jar包 | junit |
runtime | 编译时不需要,运行时需要 | mysql |
provided | 编译时需要用到,但运行时由JDK或某个服务器提供 | servlet-api |
其中,默认的 compile 是最常用的,Maven会把这种类型的依赖直接放入classpat。
<scope>test</scope>
使用Maven镜像仓库需要一个配置,在用户主目录下进入 .m2 目录,创建一 个 settings.xml 配置文件,内容如下:
<settings> <mirrors> <mirror> <id>aliyun</id> <name>aliyun</name> <mirrorOf>central</mirrorOf> <!-- 国 内 推 荐 阿 里 云 的 Maven 镜 像 --> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> </mirror> </mirrors> </settings>
构建流程
Lifecycle(生命周期)和Phase(阶段)
default内置生命周期:
validate initialize generate-sources process-sources generate-resources process-resources compile process-classes generate-test-sources process-test-sources generate-test-resources process-test-resources test-compile process-test-classes test prepare-package package pre-integration-test integration-test post-integration-test verify install deploy
default生命周期
mvn package # default生命周期,从validation ~ package mvn complie # default生命周期,从validation ~ compile
生命周期clean,执行以下3个phase:
pre-clean clean(不是生命周期,是phase) post-clean
指定多个phase
mvn clean packege # clean生命周期执行到clean,default生命周期执行到package
常用命令:
- mvn clean :清理所有生成的class和jar;
- mvn clean compile :先清理,再执行到 compile ;
- mvn clean test :先清理,再执行到 test ,因为执行 test 前必须执行 compile ,所以这里不必指定 compile ;
- mvn clean package :先清理,再执行到 package(打包)
Goal
执行一个phase又会触发多个或一个goal:如compiler:compile,相当于class中的method。
mvn tomcat:run
使用插件
执行每个phase,都是通过某个插件(plugin)来执行的,Maven本身其实并不知道如何执行 compile ,它只是负责找到对应 的 compiler 插件,然后执行默认的 compiler:compile 这个goal来完成编译。
内置的标准插件:
clean(插件名称)->clean ; compiler->compile ; surefire ->test ;jar ->package ;
一些常用的插件:
- maven-shade-plugin:打包所有依赖包并生成可执行jar;
- cobertura-maven-plugin:生成单元测试覆盖率报告;
- findbugs-maven-plugin:对Java源码进行静态分析以找出潜在问题。
<project> <build> <groupId>org.appache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> //指定Java程序入口 <cofiguration> <transformers> <transformer implementation="org.appache.maven.plugins.shade.resourse.MainfestResourseTransorformer"> <mainClass>com.itranswarp.learnjava.Main</mainClass> </transformer> </transformers> </cofiguration> </execution> </executions> </build> </project>
模块管理
Maven支持模块化管理,可以把一个大项目拆成几个模块 可以通过继承在parent的pom.xml统一定义重复配置 可以通过 < modules> 编译 多个模块
<modules> <module>模块A</module> <module>模块B</module> </modules>
使用mvnw
Maven Wrapper就是给一个项目提供一个独立的,指定版本的Maven给它使用。
安装Maven Wrapper
安装Maven Wrapper最简单的方式是在项目的根目录(即 pom.xml 所在的目录)下运行安装命令:
mvn -N io.takari:maven:0.7.6:wrapper
它会自动使用最新版本的Maven。注意 0.7.6 是Maven Wrapper的版本。最新的Maven Wrapper版本可以去官方网站查看。
如果要指定使用的Maven版本,使用下面的安装命令指定版本,例如 3.3.3
mvn -N io.takari:maven:0.7.6:wrapper -Dmaven=3.3.3
发现多了 mvnw 、 mvnw.cmd 和 .mvn 目录,我们只需要把 mvn 命令改成 mvnw 就可以使用跟项目关联的Maven。
网络编程
网络编程基础
OSI:
- 应用层:提供应用程序之间的通信;
- 表示层:处理数据格式,加解密等等;
- 会话层:负责建立和维护会话;
- 传输层:负责提供端到端的可靠传输;
- 网络层:负责根据目标地址选择路由来传输数据;
- 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。
常用协议:
IP协议是一个分组交换,它不保证可靠传输。而TCP协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP协议是 建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需 要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重 传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。
UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连 接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。
选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。
名词:
- 计算机网络:由两台或更多计算机组成的网络;
- 互联网:连接网络的网络;
- IP地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
- 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
- 网络协议:互联网使用TCP/IP协议,它泛指互联网协议簇;
TCP编程
Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连 接,而Socket内部通过TCP/IP协议把数据传输到网络。
一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配, 它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打 开。
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口, 另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连 接,双方后续就可以随时发送和接收数据。
//Server.java package ServerDemo; 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(6666); System.out.println("server is running..."); while (true) { Socket socket=ss.accept(); System.out.println("connected from "+socket.getRemoteSocketAddress()); Thread t=new Handler(socket); t.start(); } } } class Handler extends Thread{ Socket socket; public Handler(Socket socket){ this.socket=socket; } @Override public void run() { try(InputStream in=this.socket.getInputStream()){ try(OutputStream out=this.socket.getOutputStream()){ handle(in,out); } }catch (Exception e){ try { this.socket.close(); }catch (IOException ignored){ } System.out.print("client disconnected."); } } private void handle(InputStream in, OutputStream ou) throws IOException{ var writer=new BufferedWriter(new OutputStreamWriter(ou, StandardCharsets.UTF_8)); var reader=new BufferedReader(new InputStreamReader(in,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(); } } }
//Client.java package ClientDemo; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Scanner; public class Client { public static void main(String[] args) throws IOException { Socket socket=new Socket("localhost",6666); //连接指定端口 try(InputStream in=socket.getInputStream()){ try(OutputStream out=socket.getOutputStream()){ handle(in,out); } } socket.close(); System.out.println("disconnected."); } private static void handle(InputStream in, OutputStream out) throws IOException{ var writer=new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); var reader=new BufferedReader(new InputStreamReader(in,StandardCharsets.UTF_8)); Scanner sc=new Scanner(System.in); System.out.println("[server] "+reader.readLine()); for(;;){ System.out.println(">>> "); String s=sc.nextLine(); //读取一行输入 writer.write(s); writer.newLine(); writer.flush(); String resp=reader.readLine(); System.out.println("<<< "+resp); if(resp.equals("bye")){ break; } } } }
UDP编程
TCP端口和UDP端口不同,相互不占用,UDP没有建立连接,没有流的概念。
//Server.java 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); }
//Client.java DatagramSocket ds=new DatagramSocket(); ds.setSoTimeout(1000); ds.connect(IntAddress.getByName("localhost"),6666); //连接指定服务器和端口 //发送 byte[] data="Hello".getBytes(); DatagramPacket packet=new DatagramPacket(data,data.length); ds.receive(packet); String resp = new String(packet.getData(), packet.getOffset(), packet.getLength()); ds.connect()
发送邮件
MUA: Mail User Agent,用户服务的邮件代理;
MTA: Mail Transfer Agent,邮件中转代理;
MDA: Mail Delivery Agent, 邮件到达代理。
==MUA到MTA发送邮件的协议就是SMTP协议,它是Simple Mail Transport Protocol的缩写,使用标准端口25,也可以使用加密端口465或 587。==
SMTP协议是一个建立在TCP之上的协议,任何程序发送邮件都必须遵守SMTP协议。使用Java程序发送邮件时,我们无需关心SMTP协 议的底层原理,只需要使用JavaMail这个标准API就可以直接发送邮件。
加入JavaMail相关依赖:
<dependencies> <denpedency> <groupId>javax.mail</groupId> <artifactId>javax.mail-api</artifactId> <version>1.62</version> </denpedency> <denpedency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>1.62</version> </denpedency> </dependencies>
连接到SMTP服务器
//服务器地址 String smtp="smtp.office365.com"; //用户名 String username="jxsmtp101@outlook.com"; // 登 录 口 令 : String password = "********"; //连接到587端口 Properties props=new Properties(); props.put("mail.smtp.host",smtp); props.put("mail.smtp.port",587); props.put("mail.smtp.auth","true"); props.put("mail.smtp.starttls.enable","true"); //启用TLS加密 //获取Session实例 Session session=Session.getInstance(props,new Authenticator(){ protected PasswordAuthentication getPasswordAuthentication(){ return new PasswordAuthentication(username,password); } }); //设置debug模式便于调试 session.setDebug(true);
发送邮件
MimeMessage message=new MimeMessage(session); //设置发送发地址 message.setForm(new InternetAddress("me@example.com")); //设置接收方地址 message.setRecipient(Message.RecipientType.TO,new InternetAddress("xiaoming@qq.com")); //设置邮件主题 message.setSubject("Hello","UTF-8"); message.setText("Hi xiao","UTF-8"); //发送 Transport.send(message);
绝大多数邮件服务器要求发送方地址和登录用户名必须一致,否则发送将失败。
发送HTML邮件
message.setText("<h1>Hello</h1>","UTF-8","html");
发送附件
Multipair multipair=new MimeMultipair(); //添加text BodyPart textpart=new MimeBodyPart(); textpart.setContent(body,"text/html;charset=utf-8"); multipart.addBodyPart(textpart); //添加image BodyPart imagepart = new MimeBodyPart(); imagepart.setFileName(filename); imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input,"application/octet-stream"))); multipart.addBodyPart(imagepart); //设置邮件内容 message.setContent(multipair);
一个 Multipart 对象可以添加若干个 BodyPart ,其中第一个 BodyPart 是文本,即邮件正文,后面的BodyPart是附件。 BodyPart 依 靠 setContent() 决定添加的内容,如果添加文本,用 setContent("...", "text/plain;charset=utf-8") 添加纯文本,或者 用 setContent("...", "text/html;charset=utf-8") 添加HTML文本。如果添加附件,需要设置文件名(不一定和真实文件名一致), 并且添加一个 DataHandler() ,传入文件的MIME类型。二进制文件可以用 application/octet-stream ,Word文档则 是 application/msword 。
发送内嵌图片的HTML邮件
Multipart multipart = new MimeMultipart(); // 添加text: BodyPart textpart = new MimeBodyPart(); textpart.setContent("<h1>Hello</h1><p><img src=\"cid:img01\"></p>", "text/html;charset=utf-8"); multipart.addBodyPart(textpart); // 添加image: BodyPart imagepart = new MimeBodyPart(); imagepart.setFileName(fileName); imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "image/jpeg"))); // 与HTML的<img src="cid:img01">关联: imagepart.setHeader("Content-ID", "<img01>"); multipart.addBodyPart(imagepart);
在HT;ML邮件中引用图片时,需要设定一个ID,用类似 <img src="cid:img01"> 引用,然后,在添加图片作为BodyPart时,除了要正确 设置MIME类型(根据图片类型使用 image/jpeg 或 image/png ),还需要设置一个Header。
常见问题
- 如果用户名或口令错误,会导致 535 登录失败
- 如果登录用户和发件人不一致,会导致 554 拒绝发送错误
- 有些时候,如果邮件主题和正文过于简单,会导致 554 被识别为垃圾邮件的错误
接收Email
接收邮件使用最广泛的协议是POP3:Post Office Protocol version 3,它也是一个建立在TCP连接之上的协议。POP3服务器的标准端口 是110,如果整个会话需要加密,那么使用加密端口995。
另一种接收邮件的协议是IMAP:Internet Mail Access Protocol,它使用标准端口143和加密端口993。IMAP和POP3的主要区别是,IMAP 协议在本地的所有操作都会自动同步到服务器上,并且,IMAP可以允许用户在邮件服务器的收件箱中创建文件夹。
JavaMail也提供了IMAP协议的支持。
//准备登陆信息 String host="pop3.example.com"; int port=995; String username="bob@example.com"; String password="password"; Properties props=new Properties(); props.setProperty("mail.store.protocol","pop3"); //协议名称 props.setProperty("mail.pop3.host",host); //主机名 props.setProperty(("mail.pop3.port",String.valueOf(port))); //端口号 //启动ssl props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory"); props.put("mail.smtp.socketFactory.port",String.valueOf(port)); //连接到Strore URLName url=new URLName("pop3",host,port,"",username,password); Session session=Session.getInstance(props,null); session.setDebug(true); Store store=new POP3SSLStore(session,url); store.connect();
一个 Store 对象表示整个邮箱的存储,要收取邮件,我们需要通过 Store 访问指定的 Folder (文件夹),通常是 INBOX 表示收件箱:
//获取收件箱 Folder folder=store.getFolder("INBOX"); //以读写方式打开 folder.open(Folder.READ_WRITE); //打印 System.out.println("Total messages: " + folder.getMessageCount()); System.out.println("New messages: " + folder.getNewMessageCount()); System.out.println("Unread messages: " + folder.getUnreadMessageCount()); System.out.println("Deleted messages: " + folder.getDeletedMessageCount()); //获取每一封邮件 Message[] message=folder.getMessage(); for(Message message: message){ printMessage((MimeMessage)message); }
当我们获取到一个 Message 对象时,可以强制转型为MimeMessage,然后打印出邮件主题、发件人、收件人等信息:
void printMessage(MimeMessage msg) throws IOException, MessagingException { // 邮 件 主 题 : System.out.println("Subject: " + MimeUtility.decodeText(msg.getSubject())); // 发 件 人 : Address[] froms = msg.getFrom(); InternetAddress address = (InternetAddress) froms[0]; String personal = address.getPersonal(); String from = personal == null ? address.getAddress() : (MimeUtility.decodeText(personal) + " <" + address.getAddress() + ">"); System.out.println("From: " + from); // 继 续 打 印 收 件 人 : ... }
一个 MimeMessage 对象也是一个 Part 对象,它可能只包含一个文本,也可能是一个 Multipart 对象, 即由几个 Part 构成,因此,需要递归地解析出完整的正文:
String getBody(Part part) throws MessagingException,IOException{ if(part.isMimeType("text/*")){ return part.getContent().toString(); //文本 } if(part.isMimeType("multipart/*")){ Multipart multipart=(Multipart).part.getContent(); //循环解析每个子Part for(int i=0;i<multipart.getCount();i++){ BodyPart bodyPart=multipart.getBodyPart(i); String body=getBody(bodyPart); if(!body.isEmpty()){ return body; } } } return ""; }
最后关闭Folder和Store:
folder.close(true); //传入true表示删除操作会同步到服务器 store.close();
HTTP编程
HTTP是HyperText Transfer Protocol的缩写,翻译为超文本传输协议,它是基于TCP协议之上的一种请求-响应协议。
服务端总使用80端口和加密端口443。
HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。第一行总是 请求方法 路径 HTTP版本 ,例如, GET / HTTP/1.1 表示使用 GET 请求,路径是 / ,版本是 HTTP/1.1 。
后续的每一行都是固定的 Header: Value 格式,我们称为HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:
- Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别用于请求;
- User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型;
- Accept:表示客户端能处理的HTTP响应格式,text/* 表示任意文本, image/png 表示PNG格式的图片;
- Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。
如果是 GET 请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是 POST 请求,那么该HTTP请求带有Body,以一个空行 分隔。
POST /login HTTP/1.1 Host: www.example.com Content-Type: application/x-www-form-urlencoded Content-Length: 30 {"username":"bob","password":"123456"} //json格式
GET 请求的参数必须附加在URL上,并以URLEncode方式编码,例如: http://www.example.com/?a=1&b=K%26R ,参数分别 是 a=1 和 b=K&R 。因为URL的长度限制, GET 请求的参数不能太多,而 POST 请求的参数就没有长度限制,因为 POST 请求的参数必须放 到Body中。
HTTP有固定的响应代码:
- 1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;
- 2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;
- 3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请求;
- 4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效请求,404表示指定的路径 不存在;
- 5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无法响应。
import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map; public class HTTPClient { public static void main(String[] args) throws IOException { URL url=new URL("http://www.example.com/path/to/target?a=1&b=2"); HttpURLConnection connection=(HttpURLConnection)url.openConnection(); connection.setRequestMethod("GET"); connection.setUseCaches(false); connection.setConnectTimeout(5000); //请求超时5秒 //设置HTTP头 connection.setRequestProperty("Accept","*/*"); connection.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)"); //连接并发送请求 connection.connect(); //判断相应 if(connection.getResponseCode()!=200){ throw new RuntimeException("bad response"); } //获取所有响应Header Map<String, List<String>> map=connection.getHeaderFields(); for(String key: map.keySet()){ System.out.println(key + ": " + map.get(key)); } // 获 取 响 应 内 容 : // InputStream input = conn.getInputStream() } }
从Java 11开始,引入了新的 HttpClient ,它使用链式调用的API,能大大简化HTTP的处理。
package ClientDemo; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; import java.util.Map; public class HTTPClient2 { //全局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(HttpClient.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) + "..."); } }
如果我们要获取图片这样的二进制内容,只需要把 HttpResponse.BodyHandlers.ofString() 换 成 HttpResponse.BodyHandlers.ofByteArray() ,就可以获得一个 HttpResponse<byte[]> 对象。如果响应的内容很大,不希望一次性全 部加载到内存,可以使用 HttpResponse.BodyHandlers.ofInputStream() 获取一个 InputStream 流。
POST请求
// 使 用 POST 并 设 置 Body: .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
RMI远程调用
要实现RMI,服务端和客户端必须共享同一个接口。
public inteface WorldClock extends Remote{ LocalDateTime getLocalDateTime(String zoneId) throws RemoteException; }
接口必须派生java.rmi.Remote,并在每个方法声明抛出RemoteException。
public class WorldClockService implements WorldClock{ @Override public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException{ return LocalDateTime.now(zoneId.of(zoneId).withNano(0)); } }
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); //注册此服务 registry.rebind("WorldClock",skeleton); } }
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); } }
Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。因此,使用RMI时,双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。
此外,Java的RMI调用机制决定了双方必须是Java程序,其他语言很难调用Java的RMI。如果要使用不同语言进行RPC调用,可以选择更通用的协议,例如gRPC。
XML与JSON
XML
XML:可扩展标记语言,一种数据表示格式,可以描述非常复杂的数据结构,用于传输和存储数据。
格式正确的XML(Well Formed)是指XML的格式是正确的,可以被解析器正常读取。而合法的XML是指,不但XML格式正确,而且它的数据结构可以被DTD或者XSD验证。
XML是一个技术体系,除了我们经常用到的XML文档本身外,XML还支持:
- DTD和XSD:验证XML结构和数据是否有效;
- Namespace:XML节点和属性的名字空间;
- XSLT:把XML转化为另一种文本;
- XPath:一种XML节点查询语言;
使用DOM
因为XML是一种树形结构的文档,它有两种标准的解析API:
- DOM:一次性读取XML,并在内存中表示为树形结构;
- SAX:以流的形式读取XML,使用事件回调。
Java提供了DOM API来解析XML,它使用下面的对象来表示XML的内容:
- Document:代表整个XML文档;
- Element:代表一个XML元素;
- Attribute:代表一个元素的某个属性。
InputStream in=Main.class.getResourseAsStream("/book.xml"); DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance(); DocumentBuilder db=dbf.newDocumentBuilder(); Document doc=db.parse(in); void printNode(Node n, int indent) { for (int i = 0; i < indent; i++) { System.out.print(' '); } switch (n.getNodeType()) { case Node.DOCUMENT_NODE: // Document节点 System.out.println("Document: " + n.getNodeName()); break; case Node.ELEMENT_NODE: // 元素节点 System.out.println("Element: " + n.getNodeName()); break; case Node.TEXT_NODE: // 文本 System.out.println("Text: " + n.getNodeName() + " = " + n.getNodeValue()); break; case Node.ATTRIBUTE_NODE: // 属性 System.out.println("Attr: " + n.getNodeName() + " = " + n.getNodeValue()); break; default: // 其他 System.out.println("NodeType: " + n.getNodeType() + ", NodeName: " + n.getNodeName()); } for (Node child = n.getFirstChild(); child != null; child = child.getNextSibling()) { printNode(child, indent + 1); } }
使用SAX
SAX是Simple API for XML的缩写,它是一种基于流的解析方式,边读取XML边解析,并以事件回调的方式让调用者获取数据。因为是一边读一边解析,所以无论XML有多大,占用的内存都很小。
SAX解析会触发一系列事件:
- startDocument:开始读取XML文档;
- startElement:读取到了一个元素,例如``;
- characters:读取到了字符;
- endElement:读取到了一个结束的元素,例如``;
- endDocument:读取XML文档结束。
InputStream input = Main.class.getResourceAsStream("/book.xml"); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); saxParser.parse(input, new MyHandler());
class MyHandler extends DefaultHandler { public void startDocument() throws SAXException { print("start document"); } public void endDocument() throws SAXException { print("end document"); } public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { print("start element:", localName, qName); } public void endElement(String uri, String localName, String qName) throws SAXException { print("end element:", localName, qName); } public void characters(char[] ch, int start, int length) throws SAXException { print("characters:", new String(ch, start, length)); } public void error(SAXParseException e) throws SAXException { print("error:", e); } void print(Object... objs) { for (Object obj : objs) { System.out.print(obj); System.out.print(" "); } System.out.println(); } }
使用Jackson
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.10.1</version> </dependency> <dependency> <groupId>org.codehaus.woodstox</groupId> <artifactId>woodstox-core-asl</artifactId> <version>4.4.1</version> </dependency>
public class Book { public long id; public String name; public String author; public String isbn; public List<String> tags; public String pubDate; } InputStream input = Main.class.getResourceAsStream("/book.xml"); JacksonXmlModule module = new JacksonXmlModule(); XmlMapper mapper = new XmlMapper(module); Book book = mapper.readValue(input, Book.class);
使用JSON
JSON作为数据传输的格式,有几个显著的优点:
- JSON只允许使用UTF-8编码,不存在编码问题;
- JSON只允许使用双引号作为key,特殊字符用\转义,格式简单;
- 浏览器内置JSON支持,如果把数据用JSON发送给浏览器,可以用JavaScript直接处理。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.0</version> </dependency>
InputStream input = Main.class.getResourceAsStream("/book.json"); ObjectMapper mapper = new ObjectMapper(); // 反序列化时忽略不存在的JavaBean属性: mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Book book = mapper.readValue(input, Book.class); //序列化 String json = mapper.writeValueAsString(book);
要把JSON的某些值解析为特定的Java对象,例如LocalDate,也是完全可以的。
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.10.0</version> </dependency>
然后,在创建ObjectMapper时,注册一个新的JavaTimeModule:
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
自定义解析方式
public class Book { public String name; // 表示反序列化isbn时使用自定义的IsbnDeserializer: @JsonDeserialize(using = IsbnDeserializer.class) public BigInteger isbn; } public class IsbnDeserializer extends JsonDeserializer<BigInteger> { public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { // 读取原始的JSON字符串内容: String s = p.getValueAsString(); if (s != null) { try { return new BigInteger(s.replace("-", "")); } catch (NumberFormatException e) { throw new JsonParseException(p, s, e); } } return null; } }
{ "name": "Java核心技术", "isbn": "978-7-111-54742-6" }
JDBC编程
JDBC简介
JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
JDBC查询
添加一个JDBC驱动
<dependecy> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependecy>
创建数据库和表
-- 创建数据库 create database if not exists learnjdbc; # 创建登陆用户leran create user if not exists learn@'%' identified by 'leranpassword'; grant all privileges on leranjdbc.* to learn@'%' with grant option; flush privileges; # 创建表students use learnjdbc; CREATE TABLE students ( id BIGINT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT(1) NOT NULL, grade INT NOT NULL, score INT NOT NULL, PRIMARY KEY (id) ) ENGINE=INNODB DEFAULT CHARSET=UTF8; -- 插入初始数据: INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88); INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95); INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93); INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100); INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96); INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99); INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86); INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79); INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85); INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90); INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91); INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);
JDBC连接
Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。
jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2
jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8
获取数据库连接
String JDBC_URL="jdbc:mysql://localhost:3306/test"; String JDBC_USER="root"; String JDBC_PASSWORD="password"; //获取连接 Connection conn=DriverManager.getConnection(JDBC_URL,JDBC_USER,JDBC_PASSWORD); //访问数据库 //关闭连接 conn.close();
DriverManager会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。
因为JDBC连接是一种昂贵的资源,所以使用后要及时释放。使用try (resource)来自动释放JDBC连接是一个好方法:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) { try(Statement stmt=conn.createStatement()){ try(ResultSet rs=stmt.excuteQuery("SELECT id,grade,name,gender FROM students WHERE gender=\'M\' ")){ while(rs.next()){ long id = rs.getLong(1); // 注意:索引从1开始 long grade = rs.getLong(2); String name = rs.getString(3); String gender = rs.getString(4); } } } }
rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行)。
SQL注入
使用PreparedStatement可以完全避免SQL注入的问题,因为PreparedStatement始终使用?作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。
User login(String name, String pass) { ... String sql = "SELECT * FROM user WHERE login=? AND pass=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setObject(1, name); ps.setObject(2, pass); ... }
JDBC更新
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) { try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) { ps.setObject(1, 999); // 注意:索引从1开始 ps.setObject(2, 1); // grade ps.setObject(3, "Bob"); // name ps.setObject(4, "M"); // gender int n = ps.executeUpdate(); // 1,表示插入记录数量 } }
插入并获取主键
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) { try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO students (grade, name, gender) VALUES (?,?,?)", Statement.RETURN_GENERATED_KEYS)) { ps.setObject(1, 1); // grade ps.setObject(2, "Bob"); // name ps.setObject(3, "M"); // gender int n = ps.executeUpdate(); // 1 try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { long id = rs.getLong(1); // 注意:索引从1开始 } } } }
如果一次插入多条记录,那么这个ResultSet对象就会有多行返回值。
更新
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) { try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) { ps.setObject(1, "Bob"); // 注意:索引从1开始 ps.setObject(2, 999); int n = ps.executeUpdate(); // 返回更新的行数 } }
删除
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD) { try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) { ps.setObject(1, 999); // 注意:索引从1开始 int n = ps.executeUpdate(); // 删除的行数 } }
JDBC事务
数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | Yes | Yes | Yes |
Read Committed | - | Yes | Yes |
Repeatable Read | - | - | Yes |
Serializable | - | - | - |
Connection conn = openConnection(); try { // 关闭自动提交: conn.setAutoCommit(false); // 执行多条SQL语句: insert(); update(); delete(); // 提交事务: conn.commit(); } catch (SQLException e) { // 回滚事务: conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); } // 设定隔离级别为READ COMMITTED: conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
JDBC Batch
SQL数据库对SQL语句相同,但只有参数不同的若干语句可以作为batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) { // 对同一个PreparedStatement反复设置参数并调用addBatch(): for (String name : names) { ps.setString(1, name); ps.setBoolean(2, gender); ps.setInt(3, grade); ps.setInt(4, score); ps.addBatch(); // 添加到batch } // 执行batch: int[] ns = ps.executeBatch(); for (int n : ns) { System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量 } }
JDBC连接池
在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。
JDBC连接池有一个标准的接口javax.sql.DataSource
,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。常用的JDBC连接池有:
- HikariCP
- C3P0
- BoneCP
- Druid
目前使用最广泛的是HikariCP。我们以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖如下:
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>2.7.1</version> </dependency>
HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/test"); config.setUsername("root"); config.setPassword("password"); config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒 config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒 config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10 DataSource ds = new HikariDataSource(config);
注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。
try (Connection conn = ds.getConnection()) { // 在此获取连接 ... } // 在此“关闭”连接
函数式编程
函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。
Lambda基础
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" }; Arrays.sort(array, (s1, s2) -> { return s1.compareTo(s2); }); //Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
FunctionalInterface
我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
再来看Comparator接口:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); //Object方法 default Comparator<T> reversed() { return Collections.reverseOrder(this); } default Comparator<T> thenComparing(Comparator<? super T> other) { ... } ... }
方法引用
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" }; Arrays.sort(array, Main::cmp); static int cmp(String s1, String s2) { return s1.compareTo(s2); }
所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。
因为Comparator接口定义的方法是int compare(String, String),和静态方法int cmp(String, String)相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入。
Arrays.sort(array, String::compareTo); //实例方法,隐藏this
构造方法引用
List<String> names = List.of("Bob", "Alice", "Tim"); List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
使用Stream
Stream API:它位于java.util.stream包中。这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。两者对比如下:
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的byte或char | 顺序输出的任意Java对象实例 |
用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:
java.util.List | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java对象 | 惰性计算 |
int result = createNaturalStream() // 创建Stream .filter(n -> n % 2 == 0) // 任意个转换 .map(n -> n * n) // 任意个转换 .limit(100) // 任意个转换 .sum(); // 最终计算结果
创建Stream
Stream<String> stream = Stream.of("A", "B", "C", "D"); // forEach()方法相当于内部循环调用, // 可传入符合Consumer接口的void accept(T t)的方法引用: stream.forEach(System.out::println);
基于数组或Collection
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" }); Stream<String> stream2 = List.of("X", "Y", "Z").stream();
基于Supplier
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。
基本类型
Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream。
其他方法
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) { ... }
Pattern p = Pattern.compile("\\s+"); Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog"); s.forEach(System.out::println);
使用Map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
@FunctionalInterface public interface Function<T, R> { // 将T类型转换为R: R apply(T t); }
使用filter
@FunctionalInterface public interface Predicate<T> { // 判断元素t是否符合条件: boolean test(T t); }
使用Reduce
map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
@FunctionalInterface public interface BinaryOperator<T> { // Bi操作:两个输入,一个输出 T apply(T t, T u); }
int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n); int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
public static void main(String[] args) { // 按行读取配置文件: List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500"); Map<String, String> map = props.stream() // 把k=v转换为Map[k]=v: .map(kv -> { String[] ss = kv.split("\\=", 2); return Map.of(ss[0], ss[1]); }) // 把所有Map聚合到一个Map: .reduce(new HashMap<String, String>(), (m, kv) -> { m.putAll(kv); return m; }); // 打印结果: map.forEach((k, v) -> { System.out.println(k + " = " + v); }); }
输出集合
输出为List
reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange"); List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
输出为数组
List<String> list = List.of("Apple", "Banana", "Orange"); String[] array = list.stream().toArray(String[]::new);
分组输出
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots"); Map<String, List<String>> groups = list.stream() .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List。
其他操作
排序
List<String> list = List.of("Orange", "apple", "Banana") .stream() .sorted() //sorted(String::compareToIgnoreCase) .collect(Collectors.toList());
去重
List.of("A", "B", "A", "C", "B", "D") .stream() .distinct() .collect(Collectors.toList()); // [A, B, C, D]
截取
List.of("A", "B", "C", "D", "E", "F") .stream() .skip(2) // 跳过A, B .limit(3) // 截取C, D, E .collect(Collectors.toList()); // [C, D, E]
合并
Stream<String> s1 = List.of("A", "B", "C").stream(); Stream<String> s2 = List.of("D", "E").stream(); // 合并: Stream<String> s = Stream.concat(s1, s2);
flatMap
Stream<List<Integer>> s = Stream.of( Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9)); Stream<Integer> i = s.flatMap(list -> list.stream());
所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream。
并行
Stream<String> s = ... String[] result = s.parallel() // 变成一个可以并行处理的Stream .sorted() // 可以进行并行排序 .toArray(String[]::new);
其他聚合方法
除了reduce()和collect()外,Stream还有一些常用的聚合方法:
count():用于返回元素个数; max(Comparator<? super T> cp):找出最大元素; min(Comparator<? super T> cp):找出最小元素。
针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:
sum():对所有元素求和; average():对所有元素求平均数。
还有一些方法,用来测试Stream的元素是否满足以下条件:
boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件; boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素。
设计模式
创建型模式
- 工厂方法:Factory Method
- 抽象工厂:Abstract Factory
- 建造者:Builder
- 原型:Prototype
- 单例:Singleton
工厂方法
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
public final class Integer { public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } ... }
它的好处在于,valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。
MessageDigest md5 = MessageDigest.getInstance("MD5"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1");</src.size></t>