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

  1. JDK: Java Development Kit,包含JRE和编译器、调试器等开发工具。
  2. 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();

运算优先级

()
! ~ ++ -
* / %           //两个整数相除只能得到结果的整数部分
+ -
<< >> >>> 
& 
| 
+= -= *= /=
  1. 整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,得到的结果有问题。
  2. 无符号右移>>>的符号位跟着动,而>>对于负数,其符号位不变;<<则可能正数变成负数。

类型转换

  1. 类型自动提升

  2. 强制类型转换

    int i = 12345; 
    short s = (short) i; // 12345

浮点数特殊值

  • NaN: 表示Not a Number
  • Infinity
  • -Infinity

短路运算&&和||

字符和字符串

  1. 字符类型

    char c1='A';
    char c2='中';
    int n1='A';      //65
    int n2='中';      //20013
    char c3='\u0041';   //'A'十六进制
  2. 字符串类型

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

面向对象编程

类和实例

  1. class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型;
  2. 而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;    } 
}

引用变量的转型

  1. 向上转型

    Student s = new Student(); 
    Person p = s; // upcasting, ok 
    Object o1 = p; // upcasting, ok 

    instanceof()判断一个变量所指向的实例是否为指定类型,或者这个类型的子类。在向下转型时需要判断。

  2. 向下转型

    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

使用日志的好处:

  1. 可以设置输出样式,避免自己每次都写 "ERROR: " + var ;
  2. 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
  3. 可以被重定向到文件,这样可以在程序运行结束后查看日志;
  4. 可以按包名控制日志级别,只输出某些包打的日志;
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的套路如下:

  1. 对于实例变量,在 @BeforeEach 中初始化,在 @AfterEach 中清理,它们在各个 @Test 方法中互不影响,因为是不同的实例;
  2. 对于静态变量,在 @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算法,

假设甲乙双方需要传递密钥,他们之间可以这么做:

  1. 甲首选选择一个素数 p ,例如509,底数 g ,任选,例如5,随机数 a ,例如123,然后计算 A=g^a mod p ,结果是215,然后,甲 发送 p=509 , g=5 , A=215 给乙;
  2. 乙方收到后,也选择一个随机数 b ,例如,456,然后计算 B=g^b mod p ,结果是181,乙再同时计算 s=A^b mod p ,结果是121;
  3. 乙把计算的 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 个密钥,密钥管理难度大,而且非常容易泄漏。因为非对称加密的缺点就是运算速度非常慢,比对 称加密要慢很多。

在实际应用的时候,非对称加密总是和对称加密一起使用。假设小明需要给小红需要传输加密文件,他俩首先交换了各自的公钥, 然后:

  1. 小明生成一个随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发给小红;
  2. 小红用自己的RSA私钥解密得到AES口令;
  3. 双方使用这个共享的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协议为例,浏览器和服务器建立安全连接的步骤如下:

  1. 浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书;
  2. 浏览器用操作系统内置的Root CA来验证服务器的证书是否有效,如果有效,就使用该证书加密一个随机的AES口令并发送给服务 器;
  3. 服务器用自己的私钥解密获得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>
全部评论
这也太多太全太强了
点赞 回复 分享
发布于 2021-08-23 11:32

相关推荐

牛客464620405号:随便投,随便找,中国经过40多年的改革开放,人才缺口和职位空缺是巨大的,中国现在属于遍地黄金的年代,属于90后和00大机遇的时代
点赞 评论 收藏
分享
02-22 20:28
重庆大学 Java
程序员牛肉:首先不要焦虑,你肯定是有希望的。 首先我觉得你得好好想一想自己想要什么。找不到开发岗就一定是失败的吗?那开发岗的35岁危机怎么说?因此无论是找工作还是考公我觉得你都需要慎重的想一想。但你一定要避开这样一个误区:“我是因为找不到工作所以不得不选择考公”。 千万不要这么想。你这个学历挺好的了,因此你投后端岗肯定是有面试机会的。有多少人简历写的再牛逼,直接连机筛简历都过不去有啥用?因此你先保持自信一点。 以你现在的水平的话,其实如果想要找到暑期实习就两个月:一个月做项目+深挖,并且不断的背八股。只要自己辛苦一点,五月份之前肯定是可以找到暑期实习的,你有点太过于高看大家之间的技术差距了。不要焦虑不要焦虑。 除此之外说回你这个简历内容的话,基本可以全丢了。如果想做后端,先踏踏实实做两个项目再说+背八股再说。如果想考公,那就直接备战考公。 但是但是就像我前面说的:你考公的理由可以是因为想追求稳定,想追求轻松。但唯独不能是因为觉得自己找不到工作。不能这么小瞧自己和自己的学历。
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客企业服务