Java是SUN公司的詹姆斯·高斯林在90年代初开发的一种编程语言后来被Oracle收购,随着互联网的高速发展,Java逐渐成为最重要的编程语言。对于软件测试来说,掌握一两门编程语言已是必要条件,具体学习哪门编程语言还需根据自己的工作需求以及兴趣爱好来考虑。下图是TIOBE统计的编程语言长期历史排名:

Java简介

Java介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行但是不同的平台(x86、ARM等)CPU的指令集不同,因此需要编译出每一种平台的对应机器码。解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了一次编写,到处运行的效果。当然这是针对Java开发者而言,对于虚拟机需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范。从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上。Java从大到小分为三个版本:

  • Java EE (Enterprise Edition):企业版在在标准版的基础上加了大量API和库。方便Web应用Spring框架、数据库、分布式架构、消息服务等开发,和标准版使用相同虚拟机。
  • Jave SE (Standard Edition):标准版包含标准的JVM和标准库。包含JAVA核心技术
  • Java ME (Micro Edition):针对于嵌入式设备缩小JVM,无法使用SE的标准库,面向Android移动开发。

名词解释

目前在Java的最新版本中JDK已集成JRE,并自动设置环境变量。在JDK中包含编译器、调试器、JRE,在JRE中包含JVM和运行库。JDK将JAVA源码编译成JAVA字节码通过JRE运行程序。

  • JSR规范:JSR是从JVM的内存模型到Web程序接口全部都标准化的一系列规范。比如在Java平台上增加访问数据库功能需要创建JSR规范定义好接口,各个数据库厂商都能按照规范写出JAVA驱动程序,这样数据库代码就能兼容不同平台。
  • JCP组织:负责审查JSR规范的组织
  • RI:由JCP组织发布JSR规范的同时发布的参考实现,只是一个“能跑”的正确的代码,不追求速度。
  • TCK:由JCP组织发布JSR规范的同时发布的兼容性测试套件

Java基础语法

多维数组

多维数组是包含一个或多个数组的数组。
要创建一个二维数组,将每个数组添加到其各自的大括号内:
示例:

int[][] myNumbers = { {1, 2, 3, 4}, {5, 6, 7} };
int x = myNumbers[1][2];
System.out.println(x); // Outputs 7

myNumbers现在是一个以两个数组为元素的数组。
要访问myNumbers数组的元素,请指定两个索引:一个用于数组,一个用于该数组内的元素。本示例访问myNumbers的第二个数组(1)中的第三个元素(2)。
我们还可以使用for loop另一个内部for loop来获取二维数组的元素(我们仍然必须指向两个索引):
示例:

package com.lsaiah.java;

import java.util.Arrays;

public class MyClass {
    private static int[][] ns = {
            { 1, 2, 3, 4 },
            { 5, 6 },
            { 7, 8, 9 }
    };
    public static void forMethod(){
        int[][] myNumbers = { {1, 2, 3, 4}, {5, 6, 7} };
        for (int i = 0; i < myNumbers.length; ++i) {
            for(int j = 0; j < myNumbers[i].length; ++j) {
                System.out.println(myNumbers[i][j]);
            }
            System.out.println();
        }
    }
    public static void foreachMethod(){
        for (int[] arr : ns) {
            for (int n : arr) {
                System.out.print(n);
                System.out.print(",");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        forMethod();
        foreachMethod();
        System.out.println(Arrays.deepToString(ns)); //Standard library method
    }
}

Java面向对象基础

方法参数绑定

  • 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
  • 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方,因为指向同一个对象。
package com.lsaiah.java;

public class MethodDemo {
    public static void main(String[] args) {
        Person p = new Person();
        int n = 10;
        p.setAge(n); //传入n的值10
        n = 15; //n的值改为15
        p.setName("张三");
        //基本类型参数绑定:输出p.getAge()还是10
        //当set传递数组时,get指向同一个对象,当修改传递时对象改变
        System.out.println("姓名:" + p.getName() + " 年龄:" + p.getAge());
    }
}

class Person {
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private String name;
    private int age;
}

构造方法、封装

  • 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例,方法名就是类名,没有返回值;
  • 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法;
  • 可以定义多个构造方法,编译器根据参数自动判断;
  • 可以在一个构造方法内部调用另一个构造方法,便于代码复用。
package com.lsaiah.java;

public class MethodDemo02 {
    public static void main(String[] args) {
    //TODO:请给Person02类增加(String, int)的构造方法:
        Person02 p = new Person02("张三", 10);
        System.out.println("姓名:" + p.getName() + " 年龄:" + p.getAge());
    }
}

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private String name;
    private int age;
}

方法重载

  • 方法重载是指多个方法的方法名相同,但各自的参数不同;
  • 重载方法应该完成类似的功能,参考StringindexOf()
  • 重载方法返回值类型应该相同。
package com.lsaiah.java;

public class MethodDemo03 {
    public static void main(String[] args) {
    // TODO: 给Person增加重载方法setName(String, String):
        Person03 ming = new Person03();
        Person03 hong = new Person03();
        ming.setName("Xiao Ming");
        hong.setName("Xiao", "Hong");
        System.out.println(ming.getName());
        System.out.println(hong.getName());
    }
}

class Person03 {
    public String getName() {
        return name;
    }
    //方法重载:方法名、返回值类型相同,参数类型数量不同
    public void setName(String name) {
        this.name = name;
    }
    public void setName(String name, String name1) {
        this.name = name;
        this.name = name1;
    }
    private String name;
}

继承

  • 继承是面向对象编程的一种强大的代码复用方式;
  • Java只允许单继承,所有类最终的根类是Object
  • protected允许子类访问父类的字段和方法;
  • 子类的构造方法可以通过super()调用父类的构造方法;
  • 可以安全地向上转型为更抽象的类型;
  • 可以强制向下转型,最好借助instanceof判断;
  • 子类和父类的关系是is,has关系不能用继承。
//instanceof variable: 从Java 14开始支持
//在不支持的版本中编译:javac --enable-preview -source 14 Main.java 
public class Main {
    public static void main(String[] args) {
        Object obj = "hello";
        if (obj instanceof String s) {
            // 可以直接使用变量s:
            System.out.println(s.toUpperCase());
        }
    }
}
package com.lsaiah.java;

public class ExtendsDemo {
    public static void main(String[] args) {
        Person p = new Person("张三", 20);
        Student s = new Student("李四", 15, 60);
        PrimaryStudent ps = new PrimaryStudent("张三", 10, 100, 5);
        System.out.println("姓名"+ ps.getName() + "年龄"+ps.getAge() + "分数"+ ps.getScore() + "年级"+ps.getGrade());
    }
}

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

    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    protected String name;
    protected int age;
}

class Student extends Person{
    public int getScore() {
        return score;
    }
    public Student(String name, int age, int score){
        super(name,age);
        this.score = score;
    }

    protected int score;
}

class PrimaryStudent extends Student{
    public int getGrade() {
        return grade;
    }
    protected int grade;

    public PrimaryStudent(String name, int age, int score,int grade) {
        super(name, age, score);
        this.grade = grade;
    }
}

多态

  • 子类可以重写父类的方法(Override),重写在子类中改变了父类方法的行为;
  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
  • final修饰符有多种作用:
    • final修饰的方法可以阻止被重写;
    • final修饰的class可以阻止被继承;
    • final修饰的field必须在创建对象时初始化,随后不可修改。
package com.lsaiah.java;

//TODO:给一个有工资收入和稿费收入的小伙伴算税。
public class PolymorphicDemo {
    public static void main(String[] args) {
        Income[] incomes = new Income[] {
                new Income(3000),
                new Salary(7500),
                new Royalty(15000)
        };
        double totalTax = 0;
        for (Income income : incomes){
            totalTax+=income.getTax();
        }
        System.out.println(totalTax);
    }
}

class Income {
    //Income构造方法,传递参数income
    public Income(double income) {
        this.income = income;
    }
    //Income类计算税率的方法
    public double getTax(){
        return income * 0.1;
    }
    protected double income;
}

class Salary extends Income{

    public Salary(double income) {
        super(income);
    }
    @Override
    public double getTax() {
        if (income < 5000){
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class Royalty extends Income{

    public Royalty(double income) {
        super(income);
    }
    @Override
    public double getTax() {
        return 0;
    }
}

抽象类

  • 通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
  • 定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
  • 如果不实现抽象方法,则该子类仍是一个抽象类;
  • 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。
package com.lsaiah.java;

//TODO:用抽象类给一个有工资收入和稿费收入的小伙伴算税。
public class abstractDemo {
    public static void main(String[] args) {
        Income01[] incomes01 = new Income01[]{
          new Income01(3000) {
              @Override
              double getTax() {
                  return income * 0.1;
              }
          },
          new Salary01(7500),
          new Royalty01(15000)
        };
        double totalTax = 0;
        for (Income01 income01:incomes01) {
            totalTax+=income01.getTax();
        }
        System.out.println(totalTax);
    }
}

abstract class Income01{
    protected double income;
    public Income01(double income){
        this.income = income;
    };
    abstract double getTax();
}

class Salary01 extends Income01{
    public Salary01(double income) {
        super(income);
    }
    @Override
    double getTax() {
        if (income < 5000){
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class Royalty01 extends Income01{
    public Royalty01(double income) {
        super(income);
    }
    @Override
    double getTax() {
        return 0;
    }
}

接口

Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;

接口也是数据类型,适用于向上转型和向下转型;

接口的所有方法都是抽象方法,接口不能定义实例字段;

接口可以定义default方法。

abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法
package com.lsaiah.java;

//TODO:用接口给一个有工资收入和稿费收入的小伙伴算税。
public class InterfaceDemo {
    public static void main(String[] args) {
        Income02[] incomes02 = new Income02[]{
                new Salary02(7500),
                new Royalty02(15000)
        };
        double totalTax = 0;
        for (Income02 income02: incomes02) {
            totalTax+=income02.getTax();
        }
        System.out.println(totalTax);
    }
}

interface Income02 {
    double getTax();
}

class Salary02 implements Income02{
    private double income;
    public Salary02(double income){
        this.income = income;
    }
    @Override
    public double getTax() {
        if (income < 5000){
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}
class Royalty02 implements Income02{
    private double income;
    public Royalty02(double income){
        this.income = income;
    }
    @Override
    public double getTax() {
        return 0;
    }
}

静态字段和静态方法

  • 静态字段属于所有实例“共享”的字段,实际上是属于class的字段;
  • 调用静态方法不需要实例,无法访问this,但可以访问静态字段和其他静态方法;
  • 静态方法常用于工具类和辅助方法,如Arrays.sort();。
package com.lsaiah.java;

//TODO:给Person04类增加一个静态字段count和静态方法getCount,统计实例创建的个数
public class StaticDemo {
    public static void main(String[] args) {
        Person04 p4 = new Person04("张三");
        System.out.println(Person04.getCount());
        System.out.println(Person04.getCount());
        Person04 p5 = new Person04("李四");
        System.out.println(Person04.getCount());
        Person04 p6 = new Person04("王五");
        System.out.println(Person04.getCount());
    }
}

class Person04 {
    public Person04(String name) {
        this.name = name;
        count++;
    }

    public static int getCount() {
        return count;
    }

    static int count;
    static String name;
}

包/API

  • Java内建的package机制是为了避免class命名冲突;
  • JDK的核心类使用java.lang包,编译器会自动导入;
  • JDK的其它常用类定义在java.util.*java.math.*java.text.*,……;
  • 包名推荐使用倒置的域名,例如org.apache
  • 使用完整类名或使用import语句导入
  • JAVA 提供API:https://docs.oracle.com/javase/8/docs/api/

作用域

  • Java内建的访问权限包括publicprotectedprivatepackage权限;
  • Java在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;
  • final修饰符不是访问权限,它可以修饰classfieldmethod
  • 一个.java文件只能包含一个public类,但可以包含多个非public类。

访问修饰符

修饰类:

  • public:该类可以被任何其他类访问
  • default: 该类只能由同一包中的类访问。当不指定修饰符时使用。

修饰属性,方法和构造函数:

  • public:该属性方法构造函数可用于所有类
  • private: 该属性方法构造函数只能在声明的类中访问
  • default: 该属性方法构造函数只能在同一程序包中访问。当不指定修饰符时使用。
  • protected:该属性方法构造函数可在相同的包和子类中访问。

非访问修饰符

修饰类:

  • final: 该类不能被其他类继承
  • abstract: 该类不能用于创建对象(要访问抽象类,它必须从另一个类继承。

修饰属性,方法和构造函数:

  • final: 属性和方法不能被覆盖/修改
  • static: 属性和方法属于类,而不是对象,调用需要先对象实例化
  • abstract: 只能在抽象类中使用,并且只能在方法上使用。该方法没有主体,例如abstract void
    run(); 主体由子类提供(从继承)。
  • transient: 序列化包含属性和方法的对象时,将跳过这些属性和方法。
  • volatile:属性的值不在线程本地缓存,并且始终从“主内存”中读取

classpath和jar

classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。Jar包的第一层目录不能是bin目录。

  • JVM通过环境变量classpath决定搜索class的路径和顺序;
  • 不推荐设置系统环境变量classpath,始终建议通过-cp命令传入;
  • jar包相当于目录,可以包含很多.class文件,方便下载和使用;
  • MANIFEST.MF文件可以提供jar包的信息,如Main-Class,这样可以直接运行jar包。

模块

  • Java 9引入的模块目的是为了管理依赖;
  • 使用模块可以按需打包JRE;
  • 使用模块对类的访问权限有了进一步限制。

打包Jar包:

$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .

运行Jar包:

java -jar hello.jar

打包模块:

$ jmod create --class-path hello.jar hello.jmod

运行模块:

java --module-path hello.jar --module hello.world

打包JRE:

$ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/

运行JRE:

$ jre/bin/java --module hello.world

Java核心类

字符串和编码

  • Java字符串String是不可变对象;
  • 字符串操作不改变原字符串内容,而是返回新字符串;
  • 常用的字符串操作:提取子串、查找、替换、大小写转换等;
  • Java使用Unicode编码表示Stringchar
  • 转换编码就是将Stringbyte[]转换,需要指定编码;
  • 转换为byte[]时,始终优先考虑UTF-8编码。

StringBuilder

  • StringBuilder是可变对象,用来高效拼接字符串;
  • StringBuilder可以支持链式操作,实现链式操作的关键是返回实例本身;
  • StringBufferStringBuilder的线程安全版本,现在很少使用。
package com.lsaiah.java;

//TODO:请使用StringBuilder构造一个INSERT语句
public class StringBuilderDemo {
    public static void main(String[] args) {
        String[] fields = { "name", "position", "salary" };
        String table = "employee";
        String insert = buildInsertSql(table, fields);
        System.out.println(insert);
        String s = "INSERT INTO employee (name, position, salary) VALUES (?, ?, ?)";
        System.out.println(s.equals(insert) ? "测试成功" : "测试失败");
    }
    static String buildInsertSql(String table, String[] fields) {
        StringBuilder sb = new StringBuilder(1024);
        sb.append("INSERT INTO ").append(table).append(" (");
        for (int i = 0; i < fields.length; ++i) {
            if (i == fields.length - 1) {
                sb.append(fields[i]);
                continue;
            }
            sb.append(fields[i] + ", ");
        }
        sb.append(") VALUES (?, ?, ?)");
        return sb.toString();
    }
}

StringJoiner

  • 用指定分隔符拼接字符串数组时,使用StringJoiner或者String.join()更方便;
  • StringJoiner拼接字符串时,还可以额外附加一个开头和结尾。
package com.lsaiah.java;
import java.util.StringJoiner;

public class StringJoinerDemo {
    public static void main(String[] args) {
        String[] fields = { "name", "position", "salary" };
        String table = "employee";
        String select = buildSelectSql(table, fields);
        System.out.println(select);
        System.out.println("SELECT name, position, salary FROM employee".equals(select) ? "测试成功" : "测试失败");
    }

    private static String buildSelectSql(String table, String[] fields) {
        var sj = new StringJoiner(", ", "SELECT "," FROM " + table);
        for (String s : fields){
            sj.add(s);
        }
        return sj.toString();
    }
}

包装类型

Java是一种面向对象的编程语言,而基本数据类型的值不是对象,将简单数据类型的数据进行封装而得到的类就是包装类。
下表显示了原始类型和等效的包装器类:

基础数据类型 引用类型
byte java.lang.Byte
short java.lang.Short
int java.lang.Integer
long java.lang.Long
float java.lang.Float
double java.lang.Double
boolean java.lang.Boolean
char java.lang.Character
  • Java核心库提供的包装类型可以把基本类型包装为class
  • 自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5);
  • 装箱和拆箱会影响执行效率,装箱是将基础数据类型变为引用类型的赋值,拆箱是将引用类型变为基础数据类型可能发生NullPointerException
  • 包装类型的比较必须使用equals()
  • 整数和浮点数的包装类型都继承自Number
  • 包装类型提供了大量实用方法。

JavaBean

  • JavaBean是一种符合命名规范的class,它通过gettersetter来定义属性;
  • 属性是一种通用的叫法,并非Java语法规定;
  • 可以利用IDE快速生成gettersetter
  • 使用Introspector.getBeanInfo()可以获取属性列表。
package com.lsaiah.java;
import java.beans.*;
public class Main {
    public static void main(String[] args) throws Exception {
        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());
        }
    }
}

class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

枚举类

枚举类是一种特殊类,它和普通类一样可以使用构造器、定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类。

  • Java使用enum定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
  • 通过name()获取常量定义的字符串,注意不要使用toString()
  • 通过ordinal()返回常量定义的顺序(无实质意义);
  • 可以为enum编写构造方法、字段和方法
  • enum的构造方法要声明为private,字段强烈建议声明为final
  • enum适合用在switch语句中。
public class Main {
    public static void main(String[] args) {
        Weekday day = Weekday.SUN;
//        if (day.dayValue == 6 || //day.dayValue == 0) {
//            System.out.println("Today is //" + day + ". Work at home!");
//        } else {
//            System.out.println("Today is //" + day + ". Work at office!");
//        }
       switch(day){
           case MON:
           case TUE:
           case WED:
           case FRI:                            System.out.println("Today is " + day + ". Work at office!");
            break;
           case SAT:
           case SUN:
      System.out.println("Today is " + day + ". Work at home!");
            break;
           default:
               throw new RuntimeException("cannot process " + day);
       }
    }
}

enum Weekday {
    MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");

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

记录类

从Java 14开始,提供新的record关键字,可以非常方便地定义Data Class:

  • 使用record定义的是不变类;
  • 可以编写Compact Constructor对参数进行验证;
  • 可以定义静态方法。

BigInteger

  • BigInteger用于表示任意大小的整数;
  • BigInteger是不变类,并且继承自Number
  • BigInteger转换成基本类型时可使用longValueExact()等方法保证结果准确。

BigDecimal

  • BigDecimal用于表示精确的小数,常用于财务计算,scale()表示小数位数,setScale传递(小数位数,RoundingMode.HALF_UP)表示四舍五入,(小数位数, RoundingMode.DOWN)表示直接截断,stripTrailingZeros() 方法将去除末尾的0,divideAndRemainder()方法返回商和余数的数组;
  • 比较BigDecimal的值是否相等,必须使用compareTo()而不能使用equals()。返回负数、正数和0,分别表示大于、小于和等于。

常用工具类

Java提供的常用工具类有:

  • Math.abs():绝对值计算
  • Math.pow():次方计算
  • Math.sqrt():根号计算
  • Math.log():底数计算
  • Random:生成伪随机数
  • SecureRandom:生成安全的随机数

实际上真正的真随机数只能通过量子力学原理来获取,SecureRandom无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器,SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom来产生安全的随机数。

import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class Main {
    public static void main(String[] args) {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
        } catch (NoSuchAlgorithmException e) {
            sr = new SecureRandom(); // 获取普通的安全随机数生成器
        }
        byte[] buffer = new byte[16];
        sr.nextBytes(buffer); // 用安全随机数填充buffer
        System.out.println(Arrays.toString(buffer));
    }
}

异常处理

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception,或者用throws声明;
  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类;
  • Java使用异常来表示错误,并通过try ... catch捕获异常;
  • Java的异常是class,并且从Throwable继承;
  • 不推荐捕获了异常但不进行任何处理。

捕获异常

使用try语句定义可能出错的代码块,如果发生错误则执行catch语句定义的代码块,存在多个catch的时,按照顺序执行,当父类异常在前则不执行后面的子类异常,不管结果如何都会执行finally代码块,finally是可选的,一个catch语句也可以匹配多个非继承关系的异常。

package com.lsaiah.java;

public class ExceptionDemo01 {
    public static void main(String[] args) {
        try {
            int[] numbers = {1,2,3};
            System.out.println(numbers[10]);
        }catch (Exception e) {
            e.printStackTrace();
            System.out.println("ArrayIndexOutOfBoundsException");
        }finally {
            System.out.println("The 'try catch' is finished.");
        }
    }
}

抛出异常

  • 调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;
  • 捕获异常并再次抛出新的异常时,应该持有原始异常信息;
  • 通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception

throw关键字与异常类型(在Java中可用的许多异常类型:ArithmeticException,FileNotFoundException,ArrayIndexOutOfBoundsException,SecurityException等)自定义错误:

package com.lsaiah.java;

public class ExceptionDemo02 {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
//            throw new IllegalArgumentException();   //丢失原始异常NullPointerException
            throw new IllegalArgumentException(e);  //把原始的Exception实例传进去
        }
    }
    static void process2() {
        throw new NullPointerException();
    }
}

trycatch语句块中抛出异常,不会影响finally执行,JVM会先执行finally后抛出异常。如果再finally语句中抛出异常,那么catch语句的异常被屏蔽(Suppressed Exception)。

自定义异常

Java的异常是class,它的继承关系如下:

                     ┌───────────┐
                     │  Object   │
                     └───────────┘
                           ▲
                           │
                     ┌───────────┐
                     │ Throwable │
                     └───────────┘
                           ▲
                 ┌─────────┴─────────┐
                 │                   │
           ┌───────────┐       ┌───────────┐
           │   Error   │       │ Exception │
           └───────────┘       └───────────┘
                 ▲                   ▲
         ┌───────┘              ┌────┴──────────┐
         │                      │               │
┌─────────────────┐    ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘    └─────────────────┘└───────────┘
                                ▲
                    ┌───────────┴─────────────┐
                    │                         │
         ┌─────────────────────┐ ┌─────────────────────────┐
         │NullPointerException │ │IllegalArgumentException │...
         └─────────────────────┘ └─────────────────────────┘

Java标准库定义的常用异常包括:

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
  • 抛出异常时,尽量复用JDK已定义的异常类型;
  • 自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;
  • 自定义异常时,应该提供多种构造方法。
package com.lsaiah.java;
//根异常从RuntimeException派生并创建构造方法:
public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }
    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}
//其它业务异常从BaseException派生:
class UserNotFoundException extends BaseException{

    public UserNotFoundException(String error_message) {
        super(error_message);
    }
}

class LoginFailedException extends BaseException{
    public LoginFailedException(String error_mess) {
        super(error_mess);
    }
}
package com.lsaiah.java;
//在主类中调用异常
public class Main {
    public static void main(String[] args) {
        try {
            String token = login("admin", "pass");
            System.out.println("Token: " + token);
        }catch (LoginFailedException | UserNotFoundException e){
            e.printStackTrace();
        }
    }
    static String login(String username, String password){
        if (username.equals("admin")){
            if (password.equals("password")){
                return "xxx";
            }else {
                throw new LoginFailedException("登录失败:用户名或密码错误");
            }
        }else{
            throw new UserNotFoundException("用户不存在");
        }
    }
}

NullPointerException

  • NullPointerException是Java代码常见的逻辑错误,应当早暴露,早修复;
  • java -XX:+ShowCodeDetailsInExceptionMessages Main.java 可以启用Java 14的增强异常信息来查看NullPointerException的详细错误信息。
public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        System.out.println(p.address.city.toLowerCase());
    }
}

class Person {
    String[] name = new String[2];
    Address address = new Address();
}

class Address {
    String city;
    String street;
    String zipcode;
}

断言

  • 断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言。JVM默认关闭断言指令,要执行assert语句需要在执行时传递参数-enableassertions可简写-ea启用断言,还可以有选择地对特定地类-ea:com.itranswarp.sample.Main或特定的包-ea:com.itranswarp.sample...启用断言;
  • 对可恢复的错误不能使用断言,而应该抛出异常;
  • 断言很少被使用,更好的方法是编写单元测试。
public static void main(String[] args) {
    double x = Math.abs(-123.45);
    assert x >= 0 : "x must >= 0";
    System.out.println(x);
}

使用JDK Logging

Java标准库内置了日志包java.util.logging使用JDK Loggin,定义了7个日志级别:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

默认级别INFO,INFO以下的日志不会被打印出来,需要在main()方法运行之前修改配置,在JVM启动时传递参数-Djava.util.logging.config.file=<config-file-name>

  • 日志是为了替代System.out.println(),可以定义格式,重定向到文件等;
  • 日志可以存档,便于追踪问题;
  • 日志记录可以按级别分类,便于打开或关闭某些级别;
  • 可以根据配置文件调整日志,无需修改代码;
  • Java标准库提供了java.util.logging来实现日志功能。
package com.lsaiah.java;

import java.io.UnsupportedEncodingException;
import java.util.logging.Logger;

public class LoggingDemo {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(LoggingDemo.class.getName());
        logger.info("Start Process ...");
        try {
            "".getBytes("invalidCharsetName");
        } catch (UnsupportedEncodingException e) {
            // TODO: 使用logger.severe()打印异常
            logger.severe(e.toString());
        }
        logger.info("Process end ...");
    }
}

使用Commons Logging

  • Commons Logging是使用最广泛的日志模块;
  • Commons Logging的API非常简单;
  • Commons Logging是一个第三方提供的库可以自动检测并使用其他日志模块,默认使用Log4j,如果没有再使用JDK Logging,使用前先导入commons-logging-x.x.jar,在编译javac -cp commons-logging-x.x.jar Main.java和执行java -cp .;commons-logging-x.x.jar Main时需要指定classpath。

Commons Logging定义了6个日志级别:

  • FATAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG
  • TRACE
// 在静态方法中引用Log:
public class Main {
    static final Log log = LogFactory.getLog(Main.class);

    static void foo() {
        log.info("foo");
    }
}

// 在实例方法中引用Log:
public class Person {
    protected final Log log = LogFactory.getLog(getClass());

    void foo() {
        log.info("foo");
    }
}

// 在子类中使用父类实例化的log:
public class Student extends Person {
    void bar() {
        log.info("bar");
    }
}
//使用重载方法打印出异常:error(String, Throwable)
try {
    ...
} catch (Exception e) {
    log.error("got exception!", e);
}

使用Log4j

  • 导入Log4j的Jar包,通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
  • 使用Log4j只需要把log4j2.xml和相关jar放入classpath;
  • 如果要更换Log4j,只需要移除log4j2.xml和相关jar;
  • 只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。

Log4j参考配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <!-- 定义日志格式 -->
        <Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
        <!-- 定义文件名变量 -->
        <Property name="file.err.filename">log/err.log</Property>
        <Property name="file.err.pattern">log/err.%i.log.gz</Property>
    </Properties>
    <!-- 定义Appender,即目的地 -->
    <Appenders>
        <!-- 定义输出到屏幕 -->
        <Console name="console" target="SYSTEM_OUT">
            <!-- 日志格式引用上面定义的log.pattern -->
            <PatternLayout pattern="${log.pattern}" />
        </Console>
        <!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
        <RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
            <PatternLayout pattern="${log.pattern}" />
            <Policies>
                <!-- 根据文件大小自动切割日志 -->
                <SizeBasedTriggeringPolicy size="1 MB" />
            </Policies>
            <!-- 保留最近10份 -->
            <DefaultRolloverStrategy max="10" />
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <!-- 对info级别的日志,输出到console -->
            <AppenderRef ref="console" level="info" />
            <!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
            <AppenderRef ref="err" level="error" />
        </Root>
    </Loggers>
</Configuration>

使用SL4J和Logback

  • SLF4J和Logback可以取代Commons Logging和Log4j,SLF4接口更方便,Logback性能更好;
  • 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。
Commons Logging SLF4J
org.apache.commons.logging.Log org.slf4j.Logger
org.apache.commons.logging.LogFactory org.slf4j.LoggerFactory

Logback配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <file>log/output.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>log/output.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>1MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

反射

反射为了解决在运行期,对某个实例一无所知的情况下调用方法。由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。这种通过Class实例获取class信息的方法称为反射(Reflection)

Class类

  • JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;
  • 获取一个class对应的Class实例后,就可以获取该class的所有信息;
  • 通过Class实例获取class信息的方法称为反射(Reflection);
  • JVM总是动态加载class,可以在运行期根据条件来控制加载class。

获取classClass实例有三种方法:

方法一:直接通过一个class的静态变量class获取:

Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

因为Class实例在JVM中是唯一的,所以上述方法获取的Class实例是同一个实例。

  • instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,而不能对子类型比较。
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; //true,因为n是Integer类型
boolean b2 = n instanceof Number; //true,因为n是Number类型的子类
//当获取实例n时,通过n.getClass()反射获取Integer的class信息
boolean b3 = n.getClass() == Integer.class; //true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; //false,因为返回的Integer.class != Number.class
  • 如果获取到一个Class实例,可以通过该Class实例来创建对应类型class的实例。相当于new String(),通过cls.newInstance()创建的实例具有局限性,只能调用public的无参构造方法,对于带参数和非public的构造方法无法被调用。
//获取String的Class实例
Class cls = String.class;
//创建String的实例
String s = (String)cls.newInstance();
  • 利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
    factory = createLog4j();
} else {
    factory = createJdkLog();
}

boolean isClassPresent(String name) {
    try {
        Class.forName(name);
        return true;
    } catch (Exception e) {
        return false;
    }
}

访问字段

  • Java的反射API提供的Field类封装了字段的所有信息:
  • 通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()
  • 通过Field实例可以获取字段信息:getName()getType()getModifiers()
  • 通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
  • 通过反射读写字段是一种非常规方法,它会破坏对象的封装。更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下获取特定字段的值。JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)

Field getField(name):根据字段名获取某个public的field(包括父类)

Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)

Field[] getFields():获取所有public的field(包括父类)

Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

package com.lsaiah.java;
import java.lang.reflect.Field;

public class FieldDemo {
    public static void main(String[] args) throws Exception {
        Person05 p = new Person05("张三");    //创建class的实例p
        System.out.println(p.getName());    //通过实例.方法直接调用
        Class c = p.getClass(); //通过反射获取实例p的Class
        Field f = c.getDeclaredField("name"); //创建Field的实例f,通过p的Class调用getDeclaredField方法根据字段名获取当前类的某个field
        f.setAccessible(true); //设置private字段可访问
        Object value = f.get(p); //获取字段的值
        System.out.println(value);
        f.set(p,"李四");  //设置字段的值
        Object newValue = f.get(p);
        System.out.println(newValue);
    }
}
class Person05{
    public Person05(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    private String name;
}

Method getMethod(name, Class...):获取某个public的Method(包括父类)
Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
Method[] getMethods():获取所有public的Method(包括父类)
Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

  • Java的反射API提供的Method对象封装了方法的所有信息:
  • 通过Class实例的方法可以获取Method实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
  • 通过Method实例可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()
  • 通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
  • 通过设置setAccessible(true)来访问非public方法;
  • 通过反射调用方法时,仍然遵循多态原则,即总是调用实际类型的覆写方法(如果存在)。
//通过获取到Method对象调用非静态public方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args){
        String s = "Hello world";
        Method m = String.class.getMethod("substring", int.class);  //获取String substring(int)方法,参数为int
        String r = (String) m.invoke(s,6);  //调用该方法并获取结果
        System.out.println(r);
    }
}

//调用静态方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args){
        Method m = Integer.class.getMethod("parseInt", String.class);   //获取Integer.parseInt(String)方法,参数为String
        Integer n = (Integer) m.invoke(null,"12345");   //调用该静态方法并获取结果
        System.out.println(n);
    }
}

//调用非public方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args) {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name)
    }
}
class Person{
    String name;
    private void setName(String name){
        this.name = name;
    }
}

调用构造方法

  • Constructor对象封装了构造方法的所有信息;
  • 通过Class实例的方法可以获取Constructor实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()
  • 通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。

通常调用方法使用new创建新的实例:

Person p = new Person();

通过反射创建新的实例,可以通过Class提供的newInstance()方法:

Person p = Person.class.newInstance();

但是通过反射调用构造方法具有局限性,只能调用该类public无参构造方法。有参数或者不是public的构造方法就无法使用class.newInstance()调用。

为了调用任意构造方法,Java反射的API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

import java.lang.reflect.Constructor;
publci class Main{
    public static void main(String[] args){
        //获取构造方法Integer(int):
        Constructor cons1 = Integer.class.getConstructor(int.class);
        //调用构造方法:
        Integer n1 = (Integer) cons1.newInstance(123);
        System.out.print(n1);
        //获取构造方法Integer(String):
        Constructor cons2 = Integer.class.getConstructor(String.class);
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}

获取继承关系

通过Class对象可以获取继承关系:

  • Class getSuperclass():获取父类类型;
  • Class[] getInterfaces():获取当前类实现的所有接口。
  • 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

动态代理

所有interface类型的变量总是通过向上转型并指向某个实例,静态代码:

//定义接口:
public interface Hello {
    void morning(String name);
}

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

//创建实例,HelloWorld向上转型为Hello接口并调用:
Hello helo = new HelloWorld();
hello.morning("Bob");

Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;

动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main{
    public static void main(String[] args) {
        //1. 定义一个InvocationHandler实例,它负责实现接口的方法调用
        InvocationHandler handler = new InvocationHandler(){
            //3. 将返回的Object强制转换为接口
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throw Throwable {
                System.out.println(method);
                if(method.getName().equals("morning")){
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        /*
         * 2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数
         *  a. Hello.class.getClassLoader(),使用的ClassLoader,通常就是接口类的ClassLoader。
         *  b. new Class[] { Hello.class },需要实现的接口数组,至少需要传入一个接口进去
         *  c. handler,用来处理接口方法调用的InvocationHandler实例
         */
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(),
            new Class[] { Hello.class },
            handler);
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

注解

使用注解

  • 注解(Annotation)是Java语言用于工具处理的标注:
  • 注解可以配置参数,没有指定配置的参数使用默认值;
  • 如果参数名称是value,且只有一个参数,那么可以省略参数名称。
public class Hello {
    @Check(min=0, max=100, value=55)
    public int n;

    @Check(value=99)
    public int p;

    @Check(99) // @Check(value=99)
    public int x;

    @Check
    public int y;
}

@Check就是一个注解。第一个@Check(min=0, max=100, value=55)明确定义了三个参数,第二个@Check(value=99)只定义了一个value参数,它实际上和@Check(99)是完全一样的。最后一个@Check表示所有参数都使用默认值。

定义注解

  • Java使用@interface定义注解:
  • 可定义多个参数和默认值default,核心参数使用value名称;
  • 必须设置@Target来指定Annotation可以应用的范围;
  • 应当设置@Retention(RetentionPolicy.RUNTIME)便于运行期读取该Annotation
  • 非必须要元注解:@Inherited定义子类是否可继承父类定义的注解,仅针对@Target(ElementType.TYPE)类型的类注解有效,对interface无效。@Repeatable可定义注解是否可重复,经过@Repatable修饰的注解在某个类型声明处可以添加多个@Report注解。注解@Retention定义了注解的生命周期,仅编译期:RetentionPolicy.SOURCE,仅class文件:RetentionPolicy.CLASS,运行期:RetentionPolicy.RUNTIME

处理注解

可以在运行期通过反射读取RUNTIME类型的注解,注意不要漏写@Retention(RetentionPolicy.RUNTIME),否则运行期无法读取到该注解。

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

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)

使用反射API读取Annotation:

  • Class.getAnnotation(Class)
  • Field.getAnnotation(Class)
  • Method.getAnnotation(Class)
  • Constructor.getAnnotation(Class)

使用反射API读取Annotation有两种方法。方法一是先判断Annotation是否存在,如果存在,就直接读取:

Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}

第二种方法是直接读取Annotation,如果Annotation不存在,将返回null

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}

读取方法参数的Annotation可以将参数看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

要读取方法参数的注解,我们先用反射获取Method实例,然后读取方法参数的所有注解:

// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
    if (anno instanceof Range) { // @Range注解
        Range r = (Range) anno;
    }
    if (anno instanceof NotNull) { // @NotNull注解
        NotNull n = (NotNull) anno;
    }
}

如何使用注解由程序决定,如@Range注解,用来定义一个String字段的长度满足@Range参数定义:

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

在JavaBean中,可以使用该注解:

public class Person {
    @Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}

编写一个Person实例的检查方法,它可以检查Person实例的String字段长度是否满足@Range的定义:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
    // 遍历所有Field:
    for (Field field : person.getClass().getFields()) {
        // 获取Field定义的@Range:
        Range range = field.getAnnotation(Range.class);
        // 如果@Range存在:
        if (range != null) {
            // 获取Field的值:
            Object value = field.get(person);
            // 如果值是String:
            if (value instanceof String) {
                String s = (String) value;
                // 判断值是否满足@Range的min/max:
                if (s.length() < range.min() || s.length() > range.max()) {
                    throw new IllegalArgumentException("Invalid field: " + field.getName());
                }
            }
        }
    }
}

注解练习

使用@Range注解来检查Java Bean的字段。如果字段类型是String,就检查String的长度,如果字段是int,就检查int的范围。

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
//定义注解
@Retention(RetentionPolicy.RUNTIME) //定义注解声明周期为运行时(类型的注解会被加载进JVM,并且在运行期可以被程序读取。)
@Target(ElementType.FIELD)  //定义注解的位置为字段
public @interface Range {
    int min() default 0;
    int max() default 255;
}
public class Person {
    @Range(min = 1, max = 20)
    public String name;
    @Range(max = 10)
    public String city;
    @Range(min = 1, max = 100)
    public int age;

    public Person(String name, String city, int age) {
        this.name = name;
        this.city = city;
        this.age = age;
    }
    @Override
    public String toString() {
        return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
    }
}
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person("Bob", "Beijing", 20);
        Person p2 = new Person("", "Shanghai", 20);
        Person p3 = new Person("Alice", "Shanghai", 199);
        for (Person p : new Person[] { p1, p2, p3 }) {
            try {
                check(p);
                System.out.println("Person " + p + " checked ok.");
            } catch (IllegalArgumentException e) {
                System.out.println("Person " + p + " checked failed: " + e);
            }
        }
    }

    static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
        //通过 foreach循环,依次获取person这个实例的public的字段的字段名称(数据类型为Field)
        for (Field field : person.getClass().getFields()) {
            //通过Filed类的 .getAnnotation 方法来获得注解
            Range range = field.getAnnotation(Range.class);
            //判断注解是否为null
            if (range != null) {
                //使用了注解的情况下,通过Filed的 .get方法,来获取指定字段的字段值
                Object value = field.get(person);
                //如果是对 String字段使用的注解
                if (value instanceof String) {
                    String s = (String) value;
                    if (s.length()<range.min() || s.length()>range.max()) {
                        throw new IllegalArgumentException("Invalid filed: "+field.getName());
                    }
                }
                //如果是对 int字段使用的注解
                if (value instanceof Integer) {
                    int i = (int) value;
                    if (i<range.min() || i>range.max()) {
                        throw new IllegalArgumentException("Invalid filed: "+field.getName());
                    }
                }
            }
        }
    }
}

泛型

介绍

  • 泛型就是编写模板代码来适应任意类型;
  • 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
  • 注意泛型的继承关系:可以把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)。

当数组的类型被定义后,添加不同类型的元素需要强制类型转换,可能发生误转型出现ClassCastException,对不同类型的编写不同的ArrayList不全面且麻烦,为了解决问题需要使用泛型将ArrayList变成一种新的模板ArrayList<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) {...}
}

T可以是任何类型,这样只要编写一种模板就可以创建任意类型的ArrayList

//创建可存储String的ArrayList
ArrayList<String> strList = new ArrayList<String>();
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
//创建可存储Float的ArrayList
ArrayaList<Float> floatList = new ArrayList<Float>();
//创建可存储Person的ArrayList
ArrayList<Person> personList = new ArrayList<Person>();

使用泛型

  • 使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>ArrayList<Number>等;
  • 可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();
  • 不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;
  • 可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。
//自定义的类使用泛型接口实现Array.sort();
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}
class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

编写泛型

  • 编写泛型时,需要定义泛型类型<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);
    }
}

擦拭法

擦拭法决定了泛型<T>

  • 不能是基本类型,例如:int,因为实际类型是Object无法持有基本类型;
  • 不能获取带泛型类型的Class,例如:Pair<String>.class,因为T类型getClass()返回同一个Class实例,在编译后都是类<Object>
  • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>,因为不存在实际的类型.class,只有唯一的类.class;
  • 不能实例化T类型,例如:new T()擦拭后变成new Object(),需要借助Class<T>参数,通过反射来实例化T类型,使用的时候也必须传入Class<T>

泛型方法要防止重复定义方法,例如:public boolean equals(T obj)

子类可以获取父类的泛型类型<T>

extends通配符

使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似<T extends Number>定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

super通配符

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能写不能读。

使用extendssuper通配符要遵循PECS(Producer Extends Consumer Super)原则。

无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

泛型和反射

  • 部分反射API是泛型,例如:Class<T>Constructor<T>
  • 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型;
  • 可以通过Array.newInstance(Class<T>, int)创建T[]数组,需要强制转型;

同时使用泛型和可变参数时需要注意:

import java.util.Arrays;

public class Main {    
    public static void main(String[] args) {
        String[] arr = asArray("one", "two", "three");
        System.out.println(Arrays.toString(arr));
        // ClassCastException:
        String[] firstTwo = pickTwo("one", "two", "three");
        System.out.println(Arrays.toString(firstTwo));
    }
    //在pickTwo()方法内部,编译器无法检测K[]的正确类型,因此返回了Object[]
    static <K> K[] pickTwo(K k1, K k2, K k3) {
        return asArray(k1, k2);
    }
    @SafeVarargs
    static <T> T[] asArray(T... objs) {
        return objs;
    }

集合

Java集合简介

Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括ListSetMap

  • List:一种有序列表的集合,例如,按索引排列的StudentList
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的StudentSet
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Studentname查找对应StudentMap

Java集合使用统一的Iterator遍历,尽量不要使用遗留接口。

  • Hashtable:一种线程安全的Map实现;
  • Vector:一种线程安全的List实现;
  • Stack:基于Vector实现的LIFO的栈。

使用List

  • List是按索引顺序访问的长度可变的有序表,允许null元素和重复元素。优先使用ArrayList而不是LinkedList
  • 可以直接使用for each遍历List,它会自动把for each循环变成Iterator的调用,原因就在于Iterable接口定义了一个Iterator<E> iterator()方法,强迫集合类必须返回一个Iterator实例;
  • List可以和Array相互转换

ListArray

Integer[] array = list.toArray(new Integer[list.size()]);

通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法:

Integer[] array = list.toArray(Integer[]::new);

ArrayList

Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
//List接口调用List.of()方法返回只读的List,调用add()、remove()方法会抛出UnsupportedOperationException
list.add(999); // UnsupportedOperationException

练习:给定一组连续的整数,例如:10,11,12,......,20 但其中缺失一个数字,找出缺失的数字。

package com.lsaiah.java;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        // 构造从start到end的序列:
        final int start = 10;
        final int end = 20;
        List<Integer> list = new ArrayList<>();
        for (int i = start; i <= end; i++) {
            list.add(i);
        }
        // 洗牌算法suffle可以随机交换List中的元素位置:
//        Collections.shuffle(list);
        // 随机删除List中的一个元素:
        int removed = list.remove((int) (Math.random() * list.size()));
        int found = findMissingNumber(start, end, list);
        System.out.println(list.toString());
        System.out.println("missing number: " + found);
        System.out.println(removed == found ? "测试成功" : "测试失败");
    }

    static int findMissingNumber(int start, int end, List<Integer> list) {
        //List转Array:
        Integer[] nums = list.toArray(Integer[]::new);
        //数组上升排序
//        Arrays.sort(nums);
        //判断20是否在末位,数组索引从0开始
        if(nums[nums.length-1] != end){
            return end;
        }
        //判断10是否在首位
        else if(nums[0] != start){
            return start;
        }
        //此时缺失的数字一定在(10,20)中
        for (int i=1; i<nums.length;i++){
            int expectedNum = nums[i-1] + 1;
            if (nums[i] != expectedNum) {
                return expectedNum;
            }
        }
        //未缺失任何数字
        return 0;
    }
}

编写List的equals方法

  • List中查找元素时,List的实现类通过元素的equals()方法比较两个元素是否相等,因此放入的元素必须正确覆写equals()方法,Java标准库提供的StringInteger等已经覆写了equals()方法;
    • 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
    • instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false
    • 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
  • 如果不在List中查找元素,就不必覆写equals()方法。
//给Person类增加equals方法,使得IndexOf()方法返回正常
package com.lsaiah.java;
import java.util.List;
import java.util.Objects;

public class ListDemo02 {
    public static void main(String[] args) {
        List<Person06> list = List.of(
                new Person06("Xiao", "Ming", 18),
                new Person06("Xiao", "Hong", 25),
                new Person06("Bob", "Smith", 20)
        );
        boolean exist = list.contains(new Person06("Bob", "Smith", 20));
        System.out.println(exist ? "测试成功!" : "测试失败!");
    }
}

class Person06 {
    String firstName;
    String lastName;
    int age;
    public Person06(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (o instanceof Person06) {
            Person06 p = (Person06) o;
            return Objects.equals(this.firstName, p.firstName) && Objects.equals(this.lastName, p.lastName) && this.age == p.age;
        }
        return false;
    }
}

使用Map

  • Map是一种映射表,可以通过key快速查找value
  • 可以通过for each遍历keySet(),也可以通过for each遍历entrySet(),直接获取key-value
  • 最常用的一种Map实现是HashMap
//编写一个根据name查找score的程序,并利用Map充当缓存以提高查找效率:
package com.lsaiah.java;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        List<Student01> list = List.of(
                new Student01("Bob", 78),
                new Student01("Alice", 85),
                new Student01("Brush", 66),
                new Student01("Newton", 99));
        var holder = new Students(list);
        System.out.println(holder.getScore("Bob") == 78 ? "测试成功!" : "测试失败!");
        System.out.println(holder.getScore("Alice") == 85 ? "测试成功!" : "测试失败!");
        System.out.println(holder.getScore("Tom") == -1 ? "测试成功!" : "测试失败!");
    }
}
class Students {
    List<Student01> list;
    Map<String, Integer> cache;

    Students(List<Student01> list) {
        this.list = list;
        cache = new HashMap<>();
    }
    /**
     * 根据name查找score,找到返回score,未找到返回-1
     */
    int getScore(String name) {
        // 先在Map中查找:
        Integer score = this.cache.get(name);
        if (score == null) {
            // 如果找不到,就在list中找
            score = findInList(name);
        }
        // 如果找到了就加入到Map缓存中
        else {
            cache.put(name,score);
            score = this.cache.get(name);
        }
        return score == null ? -1 : score.intValue();
    }

    Integer findInList(String name) {
        for (var ss : this.list) {
            if (ss.name.equals(name)) {
                return ss.score;
            }
        }
        return null;
    }
}

class Student01 {
    String name;
    int score;

    Student01(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

编写Map的equals和hashCode

要正确使用HashMap,作为key的类必须正确覆写equals()hashCode()方法,频繁自动扩容对性能影响很大,最好创建HashMap时指定容量;

一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是:

  • 如果equals()返回true,则hashCode()返回值必须相等;
  • 如果equals()返回false,则hashCode()返回值尽量不要相等。

实现hashCode()方法可以通过Objects.hashCode()辅助方法实现。

public class Person {
    String firstName;
    String lastName;
    int age;
    @Override
    boolean equals(Object o){
        if(o instanceof Person){
            Person p = (Person) o;
            return Object.equals(firstName,lastName,age);
        }
        return false;
    }
    @Override
    int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }
}

EnumMap

如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。

使用EnumMap的时候,根据面向抽象编程的原则,应持有Map接口。

import java.time.DayOfWeek;
import java.util.*;
public class EnumDemo {
    public static void main(String[] args) {
        Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
        map.put(DayOfWeek.MONDAY, "星期一");
        map.put(DayOfWeek.TUESDAY, "星期二");
        map.put(DayOfWeek.WEDNESDAY, "星期三");
        map.put(DayOfWeek.THURSDAY, "星期四");
        map.put(DayOfWeek.FRIDAY, "星期五");
        map.put(DayOfWeek.SATURDAY, "星期六");
        map.put(DayOfWeek.SUNDAY, "星期日");
        System.out.println(map);
        System.out.println(map.get(DayOfWeek.MONDAY));
    }
}

TreeMap

Map在遍历时严格按照Key的顺序遍历,最常用的实现父类SortedMap的子类TreeMap

作为SortedMap的Key必须实现Comparable接口,StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。或者传入Comparator

要严格按照compare()规范实现比较逻辑,否则TreeMap将不能正常工作,注意到Comparator接口要求实现一个比较方法,它负责比较传入的两个元素ab,如果a<b,则返回负数,通常是-1,如果a==b,则返回0,如果a>b,则返回正数,通常是1TreeMap内部根据比较结果对Key进行排序。。

package com.lsaiah.java;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo {
    public static void main(String[] args) {
        Map<Student02, Integer> map = new TreeMap<>(new Comparator<Student02>() {
            public int compare(Student02 p1, Student02 p2) {
                if (p1.score == p2.score) {
                    return 0;
                }
                return p1.score > p2.score ? -1 : 1;
            }
        });
        map.put(new Student02("Tom", 77), 1);
        map.put(new Student02("Bob", 66), 2);
        map.put(new Student02("Lily", 99), 3);
        for (Student02 key : map.keySet()) {
            System.out.println(key);
        }
        System.out.println(map.get(new Student02("Bob", 66)));
    }
}

class Student02 {
    public String name;
    public int score;
    Student02(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public String toString() {
        return String.format("{%s: score=%d}", name, score);
    }
}

Properties

Java集合库提供的Properties用于读写配置文件.properties.properties文件可以使用UTF-8编码。

可以从文件系统、classpath或其他任何地方读取.properties文件。

读写Properties时,注意仅使用getProperty()setProperty()方法,不要调用继承而来的get()put()等方法。

# setting.properties
last_open_file=/data/hello.txt
auto_save_interval=60
//从文件读取
String f = "setting.properties";
//1. 创建Properties实例
Properties props = new Properties();
//2. 调用load()读取文件
props.load(new java.io.FileInputStream(f));
//3. 调用getProperty()读取文件配置
String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
//从classpath读取字节流
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties"));
//从内存读取字节流
String settings = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01";
ByteArrayInputStream input = new ByteArrayInputStream(settings.getBytes("UTF-8"));
Properties props = new Properties();
props.load(input);
System.out.println(props.getProperty("course"));
//写入配置文件
Properties props = new Properties();
props.setProperty("url", "http://www.lsaiah.cn");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
//load(InputStream)默认以ASCII编码读取字节流乱码,需要使用load(Reader)读取为UTF-8编码字符流
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));

Set

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

  • 放入HashSet的元素与作为HashMap的key要求相同;
  • 放入TreeSet的元素与作为TreeMap的Key要求相同;

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

遍历SortedSet按照元素的排序顺序遍历,也可以自定义排序算法。

练习:在聊天软件中,发送方发送消息时,遇到网络超时后就会自动重发,因此接收方可能会收到重复的消息,在显示给用户看的时候,需要首先去重。请练习使用Set去除重复的消息:

package com.lsaiah.java;
import java.util.*;

public class SetDemo {
    public static void main(String[] args) {
        List<Message> received = List.of(
                new Message(1, "Hello!"),
                new Message(2, "发工资了吗?"),
                new Message(2, "发工资了吗?"),
                new Message(3, "去哪吃饭?"),
                new Message(3, "去哪吃饭?"),
                new Message(4, "Bye")
        );
        List<Message> displayMessages = process(received);
        for (Message message : displayMessages) {
            System.out.println(message.text);
        }
    }
    static List<Message> process(List<Message> received) {
        // TODO: 按sequence去除重复消息
        Set<Message> treeSet = new TreeSet<>(new Comparator<Message>() {
            @Override
            public int compare(Message m1,Message m2) {
                return Integer.compare(m1.sequence, m2.sequence);
            }
        });
        treeSet.addAll(received);
        //将received重定向为一个可修改的新创建的空的ArrayList
        received = new ArrayList<>();
        //将Set的内容全数添加给received
        received.addAll(treeSet);
        return received;
    }
}
class Message {
    public final int sequence;
    public final String text;
    public Message(int sequence, String text) {
        this.sequence = sequence;
        this.text = text;
    }
}

Queue

队列Queue实现了一个先进先出(FIFO)的数据结构:

  • 通过add()/offer()方法将元素添加到队尾;
  • 通过remove()/poll()从队首获取元素并删除;
  • 通过element()/peek()从队首获取元素但不删除。

要避免把null添加到队列。

Priority Queue

PriorityQueue实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素;

PriorityQueue默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)。

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class Main {
    public static void main(String[] args) {
        Queue<User> q = new PriorityQueue<>(new UserComparator());
        // 添加3个元素到队列:
        q.offer(new User("Bob", "A10"));
        q.offer(new User("Alice", "A2"));
        q.offer(new User("Boss", "V1"));
        System.out.println(q.poll()); // Boss/V1
        System.out.println(q.poll()); // Bob/A1
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // null,因为队列为空
    }
}

class UserComparator implements Comparator<User> {
    public int compare(User u1, User u2) {
        if (u1.number.charAt(0) == u2.number.charAt(0)) {
            // 如果两人的号都是A开头或者都是V开头,比较号的大小:
            int num1 = Integer.parseInt(u1.number.substring(1, u1.number.length()));
            int num2 = Integer.parseInt(u2.number.substring(1, u2.number.length()));
            return Integer.compare(num1, num2);
            //return u1.number.compareTo(u2.number);
        }
        if (u1.number.charAt(0) == 'V') {
            // u1的号码是V开头,优先级高:
            return -1;
        } else {
            return 1;
        }
    }
}

class User {
    public final String name;
    public final String number;

    public User(String name, String number) {
        this.name = name;
        this.number = number;
    }

    public String toString() {
        return name + "/" + number;
    }
}

Deque

Deque实现了一个双端队列(Double Ended Queue),它可以:

  • 将元素添加到队尾或队首:addLast()/offerLast()/addFirst()/offerFirst()
  • 从队首/队尾获取元素并删除:removeFirst()/pollFirst()/removeLast()/pollLast()
  • 从队首/队尾获取元素但不删除:getFirst()/peekFirst()/getLast()/peekLast()
  • 总是调用xxxFirst()/xxxLast()以便与Queue的方法区分开;
  • 避免把null添加到队列。
Deque<String> deque = new LinkedList<>();

Stack

栈(Stack)是一种后进先出(LIFO)的数据结构,操作栈的元素的方法有:

  • 把元素压栈:push(E)
  • 把栈顶的元素“弹出”:pop(E)
  • 取栈顶元素但不弹出:peek(E)

在Java中,我们用Deque可以实现Stack的功能,注意只调用push()/pop()/peek()方法,避免调用Deque的其他方法。

最后,不要使用遗留类Stack

利用Stack把一个给定的整数转换为十六进制

 package com.lsaiah.java;
import java.util.ArrayDeque;
import java.util.Deque;

public class StackDemo {
    public static void main(String[] args) {
        String hex = toHex(12500);
        if (hex.equalsIgnoreCase("30D4")){
            System.out.println("测试通过");
        }else {
            System.out.println("测试失败");
        }
    }

    static String toHex(int n) {
        //创建一个空栈,通过Deque实现
        Deque<Character> dq = new LinkedList<>();
        //循环计算12500%16=0压入栈
        for (int i=n; i !=0; i= i/16){
            int remainder = i%16;
            int ch;
            if (remainder >= 10){
                ch = 65 + (remainder-10);
            }else {
                ch = remainder + 48;
            }
            dq.push(Character.valueOf((char) ch));
        }
        StringBuilder result = new StringBuilder(dq.size());
        while (dq.peek() != null){
            result.append(dq.pop());
        }
        return result.toString();
    }
}

利用Stack把字符串中缀表达式编译为后缀表达式,然后再利用栈执行后缀表达式获得计算结果

把带变量的中缀表达式编译为后缀表达式,执行后缀表达式时,传入变量的值并获得计算结果

Iterator

Iterator是一种抽象的数据访问模型。使用Iterator模式进行迭代的好处有:

  • 对任何集合都采用同一种访问模型;
  • 调用者对集合内部结构一无所知;
  • 集合类返回的Iterator对象知道如何迭代。

Java提供了标准的迭代器模型,即集合类实现java.util.Iterable接口,返回java.util.Iterator实例。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ReverseList<String> rlist = new ReverseList<>();
        rlist.add("Apple");
        rlist.add("Orange");
        rlist.add("Pear");
        for (String s : rlist) {
            System.out.println(s);
        }
    }
}

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 emptyList()
    • 创建空的Map:Map<K, V> emptyMap()
    • 创建空的Set:Set emptySet()
  • 创建单元素集合;

    • 创建一个元素的List:List singletonList(T o)
    • 创建一个元素的Map:Map<K, V> singletonMap(K key, V value)
    • 创建一个元素的Set:Set singleton(T o)
  • 创建不可变集合;

    • 封装成不可变List:List unmodifiableList(List<? extends T> list)
    • 封装成不可变Set:Set unmodifiableSet(Set<? extends T> set)
    • 封装成不可变Map:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
  • 排序/洗牌等操作;

    • Collections.sort(list);
    • Collections.shuffle(list);
  • 线程安全集合。

    • 变为线程安全的List:List synchronizedList(List list)
    • 变为线程安全的Set:Set synchronizedSet(Set s)
    • 变为线程安全的Map:Map<K,V> synchronizedMap(Map<K,V> m)

IO

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

  • 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动;

  • 字符数据以char为最小单位在Reader/Writer中单向流动。

    • 同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
    • 异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。

    Java标准库的java.io包提供了同步IO功能,java.nio提供异步功能:

  • 字节流接口:InputStream/OutputStream

  • 字符流接口:Reader/Writer

File对象

Java标准库的java.io.File对象表示一个文件或者目录:

  • 创建File对象本身不涉及IO操作;
  • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath()
  • 可以获取目录的文件和子目录:list()/listFiles()
  • 可以创建或删除文件和目录。

利用File对象列出指定目录下所有子目录和文件,并按层次打印

Documents/
&nbsp&nbspword/
&nbsp&nbsp&nbsp&nbsp1.docx
&nbsp&nbsp&nbsp&nbsp2.docx
&nbsp&nbsp&nbsp&nbspwork/
&nbsp&nbsp&nbsp&nbsp&nbsp&nbspabc.doc
&nbsp&nbspppt/
&nbsp&nbspother/

package com.lsaiah.io;
import java.io.File;
import java.io.IOException;

public class FileDemo {
    public static void main(String[] args) throws IOException {
        File currentDir = new File("C:\\Users\\Administrator.PC-20190504PBOJ\\Documents");
        listDir(currentDir.getCanonicalFile(), 0);
    }

    public static String getSpace(int level) {
        String temp = "";
        for (int i = 0; i < level; i++) {
            temp += "   ";
        }
        return temp;
    }

    static void listDir(File dir, int dir_level) {
        //先把当前目录打印出来(根据传入的目录级别打印空格)
        System.out.println(getSpace(dir_level)+dir+"\\");
        //列出所有文件和子目录
        File[] fs = dir.listFiles();
        if (fs != null) {
            for (File f : fs) {
                //判断f,如果是文件,先打印文件
                if (f.isFile()) {
                    System.out.println(getSpace(dir_level+1)+f.getName());
                }else {
                    //如果是目录,继续递归执行
                    listDir(f,dir_level+1);
                }
            }
        }
    }
}

InputStream

OutputStream

Fitter模式

操作Zip

读取classpath资源

序列化

Reader

Writer

PrintStream和PrintWriter

日期与实践

单元测试

正则表达式

加密与安全

多线程

Maven基础

网络编程

XML与JSON

JDBC编程

函数式编程

设计模式

Web开发

内部类

嵌套类(类中的一个类),要访问内部类需要先创建外部类的对象,然后创建内部类的对象。
示例:

class OuterClass {
    int x = 10;
    class InnerClass {
        int y = 5;
        public int myInnerMethod() {
            return x;
        }
    }
}
public class MyMainClass {
    public static void main(String[] args) {
        OuterClass myOuter = new OuterClass();
        OuterClass.InnerClass myInner = myOuter.new InnerClass();
        System.out.println(myInner.y + myOuter.x);
        System.out.println(myInner.myInnerMethod());
    }
}

当Private修饰内部类时,外部对象将无法访问内部类。
当Static修饰内部类时,可以不创建外部类对象访问内部类,无法访问外部类的成员。

关键字

关键字 含义
abstract 表明类或者成员方法具有抽象属性
assert 断言,用来进行程序调试
boolean 基本数据类型之一,声明布尔类型的关键字
break 提前跳出一个块
byte 基本数据类型之一,字节类型
case 用在switch语句之中,表示其中的一个分支
catch 用在异常处理中,用来捕捉异常
char 基本数据类型之一,字符类型
class 声明一个类
const 保留关键字,没有具体含义
continue 回到一个块的开始处
default 默认,例如,用在switch语句中,表明一个默认的分支。Java8 中也作用于声明接口函数的默认实现
do 用在do-while循环结构中
double 基本数据类型之一,双精度浮点数类型
else 用在条件语句中,表明当条件不成立时的分支
enum 枚举
extends 表明一个类型是另一个类型的子类型。对于类,可以是另一个类或者抽象类;对于接口,可以是另一个接口
final 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量
finally 用于处理异常情况,用来声明一个基本肯定会被执行到的语句块
float 基本数据类型之一,单精度浮点数类型
for 一种循环结构的引导词
goto 保留关键字,没有具体含义
if 条件语句的引导词
implements 表明一个类实现了给定的接口
import 表明要访问指定的类或包
instanceof 用来测试一个对象是否是指定类型的实例对象
int 基本数据类型之一,整数类型
interface 接口
long 基本数据类型之一,长整数类型
native 用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的
new 用来创建新实例对象
package
private 一种访问控制方式:私用模式
protected 一种访问控制方式:保护模式
public 一种访问控制方式:共用模式
return 从成员方法中返回数据
short 基本数据类型之一,短整数类型
static 表明具有静态属性
strictfp 用来声明FP_strict(单精度或双精度浮点数)表达式遵循[IEEE 754](https://baike.baidu.com/item/IEEE 754)算术规范
super 表明当前对象的父类型的引用或者父类型的构造方法
switch 分支语句结构的引导词
synchronized 表明一段代码需要同步执行
this 指向当前实例对象的引用
throw 抛出一个异常
throws 声明在当前定义的成员方法中所有需要抛出的异常
transient 声明不用序列化的成员域
try 尝试一个可能抛出异常的程序块
void 声明当前成员方法没有返回值
volatile 表明两个或者多个变量必须同步地发生变化
while 用在循环结构中

错题集

  • 多维数组求平均数
package com.lsaiah.java;

public class Main {
    public static void main(String[] args) {
        // 用二维数组表示的学生成绩:
        int[][] scores = {
                {82, 90, 91},
                {68, 72, 64},
                {95, 91, 89},
                {67, 52, 60},
                {79, 81, 85},
        };
        //外循环控制一维数组
        for(int i=1;i<scores.length;i++){
            double sum = 0;//定义一个初始化sum
            //内循环控制元素里面的数组
            for(var j=0;j<scores[i].length;j++){
                sum+=scores[i][j];//每个元素里面的数组相加
            }
            double avg = sum /scores[i].length;//求每个元素的平均数
            System.out.println("第"+ i +"班的平均成绩是:"+avg);//输出每个元素的平均数
        }
    }
}
Last modification:April 9th, 2020 at 05:40 pm
如果觉得我的文章对你有用,请随意赞赏,感谢!