Java基础(上)


Java基础(上)

原码、反码、补码

原码:十进制数据的二进制表示,最左边是符号位,0表示正数,1表示负数。在计算机中,8个 bit 为一个字节,是最小的存储单元。

利用原码对正数进行计算是不会有问题的。但是对于负数,假设现在为-0,即10000000,再在其基础上加1,则会变成10000001,十进制为-1。继续进行下去的话,得到的结果完全相反。为了解决原码不能计算负数的问题,故出现了反码。

反码:正数反码等于原码,负数的反码在原码的基础上,符号位不变,数值取反。

例如:-56 原码为:10111000,则其反码为:11000111。现在我要计算-56+1,利用反码直接加,得到11001000。而-55的反码是11001000,计算正确。但是,注意到如下表格:

十进制 原码 反码
+1 00000001 00000001
+0 00000000 00000000
-0 10000000 11111111
-1 10000001 11111110

反码在计算的时候,-1跨越到+1,需要多跨一次,因为这个时候0有两个,分别是+0和-0。这意味着,如果我们的计算跨0了,会产生一次计算的误差,因为反码中0有两种表示

补码:正数的补码等于原码,负数的补码在反码的基础上加1。

加1后,上述表格修正为:

十进制 原码 反码 补码
+1 00000001 00000001 00000001
+0 00000000 00000000 00000000
-0 10000000 11111111 00000000
-1 10000001 11111110 11111111

观察到,+0和-0的补码被修正为00000000,反码产生的的误差,被消除了。

将上述表格推广有:

十进制 原码 反码 补码
127 01111111 01111111 01111111
+1 00000001 00000001 00000001
0 00000000 00000000 00000000
-126 11111110 10000001 10000010
-127 11111111 10000000 10000001
-128 10000000

故1个字节(8位)的取值范围是 -128 ~ 127。并且,计算机在底层,存的是补码。

补充:

  1. byte 占1个字节,short 占2个字节,int 占4个字节,long 占8个字节。
  2. 对于左移:正数负数都是补0;对于右移:正数补0,负数补1。
  3. 对于无符号右移(>>>),高位补0。

String相关

String,StringBuilder 和 StringBuffer

对于 String 而言:

  • String 类的底层实现是基于数组和字符串常量池(串池),利用不可变性(final 修饰)提供了安全的操作。JDK9 之前,String 是使用 char 数组进行存储,JDK9 之后是利用 Byte 数组进行存储,节省了内存空间。String 设计成不可变的原因主要有:保证线程安全,防止数据被意外修改,可以对字符串进行复用,代码设计更加简单。

  • String 的赋值操作是将串直接开在堆区中的串池,并且串池中的内容是可以被复用的。例如String str1 = "abc";String str2 = "abc";,str1 和 str2 的内容一致,且都是直接使用等号进行赋值,故在串池中只有一个 abc 串,str1 和 str2 共用这个 abc 串,即 str1 和 str2 二者维护的串的地址值一致,也可以理解为 str2 是 C++ 中 str1 的引用。

  • String 的 new 操作不会进行复用。利用 new 操作符创建出来的字符串会被直接开辟在堆区,例如String str1 = new String("abc");String str2 = new String("abc");二者均用 new 操作符,虽内容相等,但此时地址值却不同了。

  • String 的值无法被修改,但可以被共享(例如串池中的字符串)。故字符串的拼接操作比较繁琐。如果是纯字符串拼接(即没有变量参与),则会触发系统的字符串优化机制,例如 String str = "a" + "b" + "c";是纯字符串拼接,则触发优化机制,编译的时候转化为String str = "abc";。假如有变量参与,例如 String s1 = "a"; String s2 = s1 + "b"; String s3 = s2 + "c";。这种情况下,在 JDK8 之前,需要先 new 一个 StringBuilder 对象来接收s1 + "b",再 new 一个 String 来接收拼接后的s2。而后再 new 一个 StringBuilder 来接收 s2 + "c",最后再 new 一个 String 来接收拼接后的 s3 。故会很浪费内存和性能。而在 JDK8 之后,对于含变量的字符串拼接,系统会先预估拼接后字符串长度,开一个 char 数组来接收,虽说不需要再创建额外对象,但是预估长度这个操作还是会浪费时间。故 String 适合用于少量拼接或者不拼接下的字符串操作。

  • 如果要较方便的改变字符串内容,一种比较可行的方式是先将 String 转成 char 数组char[] arr = str.toCharArray();,对 char 数组的内容进行修改,而后再将修改后的 char 数组转成 String String result = new String(arr);

常用方法

方法名 说明
toUpperCase() / toLowerCase() 大小写转换
String[] split(String regex) 依照 regex 对字符串进行分割
trim() 去掉字符串左右空格
boolean equalsIgnoreCase(String str) 忽略大小写比较两个字符串是否一致
boolean startsWith(String str) / endsWith(String str) 字符串是否以 str 开头或者结尾
replace(Char oldchar, Char newchar) 用新的单字符替换旧字符

对于 StringBuilder 而言:

  • StringBuilder 是一个可变的字符串容器,其容量最大为$2^{31}-1$,类似于 C++ 中的 vector 容器,StringBuilder 在容量不足的情况下会动态扩容,扩容为:新容量 = 旧容量 * 2 + 2。内部维护一个字节数组,在拼接的时候直接往后方增加内容,容量不足时自动扩容,故对于拼接操作,StringBuilder 并不会产生新的额外的对象和字符串,故若字符串需要频繁进行拼接操作,使用 StringBuilder 的效率会快很多。

对于 StringBuffer 而言:

  • StringBuffer 和 StringBuilder 基本类似。相对于 StringBuilder ,StringBuffer 是线程安全的,属多线程操作字符串。StringBuffer 类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。而 StringBuilder 是单线程操作字符串,线程不安全,但因为 StringBuffer 需要加锁,所以效率上不如 StringBuilder。
public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "ab";
        String s3 = s2 + "c";
        System.out.println(s1 == s3);     //false
        //原因是含有变量s2的字符串拼接,JDK8之前会new StringBuilder来完成拼接
        //JDK8之后会预估拼接后的总大小,将其存放入数组中
        //上述两种操作都是new了新对象出来,故地址值不同,fasle
    }
}
public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "a" + "b" + "c";
        System.out.println(s1 == s2);   //true
        //结果是true
        //原因是没有变量参与的字符串拼接,在编译的时候,会拼接为“abc”
        //复用串池的字符串,地址值一致,故true
    }
}

StringBuilder

public class Main {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("abcd");

        //增加
        sb.append("efg");
        sb.append('h');
        sb.append(sb);
        System.out.println(sb); //abcdefghabcdefgh

        //反转
        sb.reverse();
        System.out.println(sb); //hgfedcbahgfedcba

        //删除
        sb.delete(0, 5);
        System.out.println(sb); //cbahgfedcba

        //替换
        sb.replace(0, 5, "代换字符串");
        System.out.println(sb); //代换字符串fedcba

        //截取子串
        String subStr = sb.substring(0, 5);
        System.out.println(subStr); //代换字符串

        //StringBuilder没有重写equals方法,默认比较地址值
        StringBuilder sb1 = new StringBuilder("abc");
        StringBuilder sb2 = new StringBuilder("abc");
        System.out.println(sb1.equals(sb2));    //false

        //删除指定字符
        sb.deleteCharAt(0);
        System.out.println(sb); //换字符串fedcba

        //检索
        int pos1 = sb.indexOf("字符串");
        System.out.println(pos1);    //1
        int pos2 = sb.indexOf("字符串", 5);
        System.out.println(pos2);   //-1
        int pos3 = sb.lastIndexOf("a");
        System.out.println(pos3);   //9
        int pos4 = sb.lastIndexOf("a", 8);
        System.out.println(pos4);   //-1
        
    }
}

底层原码分析:

  • 默认容量为16,添加的内容长度小于16,直接存。
  • 空间不够则扩容,扩容为原来的两倍 + 2。
  • 如果一次性添加的长度过长,则以实际长度为准。
//默认空参构造,创建长度为16的字节数组
public StringBuilder() {
    super(16);
}


//super对父类的构造如下:
AbstractStringBuilder(int capacity) {
    if (COMPACT_STRINGS) {
        value = new byte[capacity];	//创建了byte数组
        coder = LATIN1;
    } else {
        value = StringUTF16.newBytesFor(capacity);
        coder = UTF16;
    }
}

//append方法,调用父类的重载方法,并将添加后的自身返回出去
public StringBuilder append(String str) {
    super.append(str);
    return this;
}


//父类的append重载方法
public AbstractStringBuilder append(String str) {
    if (str == null) {	//如果str为空,则调用appendNull添加“null”
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);	//传入append之后至少需要的长度
    putStringAt(count, str);
    count += len;
    return this;
}


//判断长度是否需要扩容
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    int oldCapacity = value.length >> coder;	//获取旧的长度
    //coder是为了适应多种编码方式,这里为了理解方便可以忽略不看
    if (minimumCapacity - oldCapacity > 0) {	//如果新的长度比旧的长
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity) << coder);	//扩容
    }
}


//调用newCapacity扩容
private int newCapacity(int minCapacity) {
    int oldLength = value.length;
    int newLength = minCapacity << coder;
    int growth = newLength - oldLength;	//至少需要增加的长度
    //调用newLength方法判断最终需要多长的长度
    int length = ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));
    if (length == Integer.MAX_VALUE) {	//StringBuilder的容量是int的最大值
        throw new OutOfMemoryError("Required length exceeds implementation limit");
    }
    return length >> coder;
}

StringJoiner

StringJoiner 帮助我们以更方便的形式对字符串进行格式化拼接,并且也能提高字符串的操作效率。

方法名 说明
public StringJoiner(间隔符号) 创建StringJoiner对象,指定拼接时的间隔符号
public StringJoiner(间隔符号,开始符号,结束符号) 创建StringJoiner对象,指定拼接时的间隔符号,开始符号和结束符号
public StringJoiner add(String str) 添加数据
public int length() 返回长度
public String toString() 返回字符串
import java.util.Collections;
import java.util.StringJoiner;

public class Main {
    public static void main(String[] args) {
        //创建对象,并指定中间的间隔符号
        StringJoiner sj = new StringJoiner(",");
        //添加元素
        sj.add("aaa");
        sj.add("bbb");
        System.out.println(sj); //aaa,bbb

        //创建对象,并指定中间,开始和结束符号
        StringJoiner sj1 = new StringJoiner(", ", "[", "]");
        sj1.add("aaa");
        sj1.add("bbb");
        sj1.add("ccc");
        System.out.println(sj1);    //[aaa, bbb, ccc]
    }
}

面向对象

eclipse中可以使用 Alt + Shift + s + r 的快捷键来快速生成 set 和 get 方法。

小细节:类名首字母要大写。且一个 Java 文件中可以定义多个 class 类,且只能一个类是 public 修饰,且 public 修饰的类名必须成为代码文件名。实际开发中建议还是一个文件定义一个 class 类。

封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为。(例如对于一个“圆”类,“画圆”应该是封装在“圆”这个类中)

创建对象:创建对象是 new 关键字的工作,在创建对象的时候,虚拟机会自动调用构造方法,作用是给成员变量进行初始化。

对象关系

对象关系分为:依赖、关联、聚合、组合、继承、实现。

  • 依赖:体现为局部变量、方法的参数或者是对静态方法的调用,某个类需要另一个类才能工作。
  • 关联:一个类作为另一个类的成员变量。
  • 聚合:一个类的数组作为另一个类的成员变量。
  • 组合:这几个类一起产生,具有同一生命周期
  • 继承:使用了 extends 关键字。
  • 实现:使用了 implements 关键字。

Javabean类、测试类、工具类

用来描述一类事物的类,叫做 Javabean 类(例如:Student,Teacher,Dog,Cat……), Javabean 类是不写main方法的。

标准的 javabean 类:

  1. 类名需要见名知意。

  2. 成员变量用 private 修饰。

  3. 至少提供两个方法:无参构造和全部参数构造。

  4. 提供每一个成员变量的 set 和 get 方法,如果还有其他行为,也需要写上。

用来检查其他类是否书写正确,带有main方法的类叫做测试类,是程序的入口

例如你书写了一个叫做 QuickSort 的类,接下来你要测试这个类的性能,可以在项目中再开一个带有 main 方法的类,起名为 test,用来测试 QuickSort 这个类的各种性能,那么对应的,这个 test 类,就是测试类。

工具类不是用来描述一类事物的,而是帮我们做一些事情(例如:Math,ArrUtil……)。

工具类在书写的时候需要注意,我们要私有化构造方法,因为在工具类中,让外界创建对象是没有意义的。但是,工具类的方法要定义为静态的,方便我们进行调用。

创建对象时:

  1. 加载 class 文件(把对象的成员属性都加载都方法区中)。

  2. 局部变量入栈。

  3. 堆内存中开辟空间(new 在堆区开辟空间)。

  4. 堆区变量默认初始化(null)。

  5. 显示初始化(定义 class 时还定义了属性值,则按照属性值赋初值)。

  6. 构造方法初始化。

  7. 堆内存的值赋给栈中的局部变量。

Static 变量、方法

类当中的 static 变量是共享的,可以直接通过类名调用,当然,我们也更加推荐使用类名调用。static 变量存放在堆区中的静态区里,并且是比对象先创建出来的,类中有 static 类时,先会先创建 static 类。

public class Student {
    public String name;
    public int age;
    public static String teacherName;
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        Student stu1 = new Student("小明", 18);    
        Student stu2 = new Student("小红", 19);
        //静态变量teacherName在对象stu1创建之前已经被创建出来了,为null
        Student.teacherName = "大明";        //类名调用
        System.out.println(stu1.teacherName);
        System.out.println(stu2.teacherName);
        /*打印内容如下;
            大明
            大明
        */
    }
}

静态方法只能访问静态变量和静态方法,调用共享内容,不明确具体对象,故静态方法中没有 this 关键字。

非静态的方法体和对象挂钩,需要调用某个特性对象的属性,故有 this 关键字。

public class Student {
    public String name;
    public int age;
    public static String teacherName;
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void print1() {
        //不是静态方法,涉及到具体对象,故可以使用this
        System.out.println(this.name + " " + this.age + " " + this.teacherName);
    }
    public static void print2() {
        //System.out.println(name + " " + age);
        //上述代码是错误的,因为static是共享的,无法明确打印哪个对象的name和age
        System.out.println(teacherName);
    }
}

重新认识 main 方法:

public class Main {
    public static void main(String[] args) {

        //public: 被JVM调用,访问权限需要足够大
        //static: 被JVM调用,不用创建对象,直接类名访问
        //因为main方法是静态的,所以测试类中其他方法也需要是静态的
        test();
        //void: 被JVM调用,不需要给JVM返回值
        //main: 一个通用的名称,虽然不是关键字,但是被JVM识别
        //String[] args: 以前用于接收键盘录入数据的,现在没有用了
    }

    public static void test() {    //需要是静态的,因为静态方法只能调用静态方法
        System.out.println("test调用");
    }
}

继承

继承特点:

  • 构造方法:父类非私有和私有的构造方法子类都不能继承。但是子类在初始化的时候,有可能用到父类中的数据,如果父类没有完成初始化,则子类也无法使用对应的数据。所以,子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化。(虚拟机会在子类的构造方法的第一行加入 super()来保证子类会去调用父类的构造方法,但是这个时候调用的是父类的无参构造,如果想要调用父类的有参构造,可以手动往里填参数,调用带参构造)

  • 成员变量:子类可以继承父类非私有和私有的成员变量,但子类无法调用父类私有的成员变量。(可以利用父类的 get 和 set 方法获得父类对应的成员变量)

  • 成员方法:子类可以继承父类非私有的成员方法,但无法继承父类私有的成员方法。

  • 虚方法表:每一个类都会将非 private 、非 static 、非 final 修饰的方法记录到自己的虚方法表中,若有子类对自身进行继承,则会直接将自己的虚方法表传给子类,而后子类在这个虚方法表的基础上再去添加自己的虚方法。

访问特点:

  • 成员变量访问特点:就近原则,谁近访问谁,可以通过 this 关键字访问自身变量,通过 super 关键字直接访问到父类变量,但无法利用两个 super 访问祖类变量。

  • 成员方法访问特点:同上。

方法的重写:

在继承体系中,子类出现了和父类一模一样的方法声明,我们就称子类这个方法是重写的方法。当父类的方法不能满足子类现在的需求时,需要进行方法重写。

在进行方法重写时,我们需要在重写的方法上面加上注解@Override表示这个方法是重写的方法,并且检查重写的方法语法是否正确,不写也可以。

如果发生了重写,则会覆盖掉父类给的虚方法表中的方法。

静态方法无法被重写。

重写时:

  • 重写方法的名称、形参列表必须与父类中的一致。

  • 子类的方法权限必须大于等于父类(空着不写 < protected < public )。

  • 返回值类型子类必须小于等于父类。

  • 只有被添加到虚方法表中的方法才能够被重写。

多态

多态的表现形式:父类类型 对象名称 = 子类对象;

多态的前提:有继承关系;有父类引用指向子类对象;有方法重写

如果具有 static 关键字,则无法触发多态,因为 static 修饰下是对于类本身而言,而不是具体对象而言。

多态调用成员变量编译看左边,运行也看左边。

  • 编译看左边:javac 编译代码的时候,先看左边的父类中有没有这个变量。如果有,编译成功,否则就编译失败。

  • 运行看左边:java 运行代码的时候,实际获取的是左边父类中成员变量的值。例如:Animal a = new Dog(); ,使用 a.name 时,调取出来的是父类的名字。

多态调用成员方法编译看左边,运行看右边。

  • 编译看左边:javac 编译代码的时候,会看左边的父类中有没有这个方法。如果有,编译成功,否则就编译失败。

  • 运行看右边:java 运行代码的时候,实际上运行的是子类的方法。

多态的优势:

  • 在多态形势下 ,右边对象可以实现解耦合,便于扩展和维护。例如:Person p = new Student(); p.work();,若我想改变 work 的调用者,那我只需要改 new 后面的内容就好了。

  • 定义方法时,使用父类作为参数,可以接收所有子类对象,体现多态的扩展性和便利。

多态的弊端:

  • 不能调用子类的特有方法(即子类有,父类没有的方法)。

  • 原因是当调用成员方法时,编译看左边,运行看右边。编译的时候看到左边的父类没有特有方法,就会报错。

  • 解决方法:把左侧父类强制转换为子类。子类类型 子类名称 = (子类类型) 父类名称;

多态的类型判断:

多态可以使用 instanceof 关键字进行类型的判断,语法为:父类名称 instanceof 子类类型

新特性下,可以使用父类名称 instanceof 子类类型 子类名称将父类强转为子类。

协变

协变指的是子类在重写父类的方法的时候可以更改对应方法的返回值类型,使得这个返回值类型变得更加具体

class Grain {
  public String toString() { 
	  return "Grain"; 
  }
}

class Wheat extends Grain {
  public String toString() { 
	  return "Wheat";
  }
}

class Mill {
  Grain process() { 
      //父类返回的是Grain类型
	  return new Grain();
  }
}

class WheatMill extends Mill {
  Wheat process() {	
      //WheatMill子类重写了父类的process方法,返回的类型是Wheat
      //Wheat是Grain的子类,也可以说process返回的类型更加具体了
	  return new Wheat();
  }
}

public class CovariantReturn {
  public static void main(String[] args) {
    Mill m = new Mill();
    Grain g = m.process();
    System.out.println(g);
    m = new WheatMill();
    g = m.process();
    System.out.println(g);
  }
}
/* 最终输出为:
 * Grain
 * Wheat
 * */

包就是文件夹。用来管理各种不同功能的 java 类,方便后期代码维护。

包名的规则:公司域名的反写 + 包的作用,需要全部大小写,见名知意。

使用导包的情况

  • 使用同一个包中的类时,不需要导包。

  • 使用 java.lang 包中的类时,不需要导包。

  • 其他情况需要导包。

  • 如果同时使用两个包中的同名类,需要用全类名。

Final

final 可以修饰方法、类和变量:

  • 修饰方法:表明该方法是最终方法(或者理解为一种规则),不能被子类重写。
public class Father {
    public final void show() {
        System.out.println("父类被final修饰的show方法");
    }
}
public class son extends Father {
//    @Override
//    public void show() {
//        ...
//    }
//以上是错误的,因为final修饰过的方法无法被重写
}
  • 修饰类:表明该类是最终类,不能被继承。
public final class Father {
    public final void show() {
        System.out.println("父类被final修饰的show方法");
    }
}

//class Son extends Father {
//    错误的,因为final修饰过的类无法被继承
//}

Java 的 String 类就是 final 修饰的类,String 里面维护了一个 private final byte[] value;,final 修饰符保证了数组地址值无法改变,private 修饰符保证了数组内容无法改变。

  • 修饰变量:表明该变量为常量,只能被赋值一次。如果是修饰基本数据类型,则变量存储的数据值不能发生改变;如果是修饰引用数据类型,则变量存储的地址值不能发生改变,但内部的数据值是可以发生改变的。

常量

实际开发中,常量一般作为系统的配置信息,方便维护,提高可读性。

常量命名规范:

  • 单个单词:全部大写。

  • 多个单词:全部大写,单词之间用下划线隔开。

权限修饰符

private < 空着不写(缺省/默认)< protected < public

private:只能自己用。

默认:只能本包中用,别的包用不了。

protected:自己用,本包用,不同包下的子类也可以用。

public:公共的。

一般开发中使用 private 和 public。如果方法中的代码是抽取其他方法中共性代码,这个方法也设为私有。

代码块

代码块分三种:

  • 局部代码块:在代码中利用大括号对部分代码进行分块,目的是提前结束变量的生命周期

  • 构造代码块:将构造函数中重复的代码抽取出来,分块。但是这么处理比较死板,一种比较灵活的方式是利用 this 关键字进行代码调用。

public class Student {
    private String name;
    private int age;
    {
        System.out.println("开始创建对象了");
    }
    public Student() {
        System.out.println("无参构造");
    }
    public Student(String name, int age) {
        System.out.println("有参构造");
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        Student stu1 = new Student();
        Student stu2 = new Student();
        /*
         打印内容如下:
            开始创建对象了
            无参构造
            开始创建对象了
            无参构造
         */
    }
}

使用 this 关键字优化:

public class Student {
    private String name;
    private int age;
    public Student() {
        this("null", 0);
    }
    public Student(String name, int age) {
        System.out.println("开始创建对象了");
        this.name = name;
        this.age = age;
    }
}
  • 静态代码块:格式:static{},需要通过 static 关键字修饰,随着类的加载而加载,并且自动触发,只执行一次,并且是在开始的时候执行的。使用场景:在类加载的时候,做一些数据初始化。
public class Student {
    private String name;
    private int age;
    static {
        System.out.println("静态代码块加载了");
    }
    public Student() {
        System.out.println("无参构造");
    }
    public Student(String name, int age) {
        System.out.println("有参构造");
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        Student stu1 = new Student();
        Student stu2 = new Student();
        /*打印内容如下:
            静态代码块加载了
            无参构造
            无参构造
        */
    }
}

执行顺序:父类静态代码块和静态变量(哪个先写哪个先执行)、子类静态代码块和静态变量(同上)、父类构造代码块和成员变量(哪个先写哪个先执行)、父类构造方法、子类构造代码块和成员变量(同上)、子类构造方法。

抽象类和抽象方法

一个方法抽取到父类中,若不明确方法体,则利用 abstract 关键字修饰,abstract 修饰过的方法为抽象方法,包含抽象方法的类必须声明为抽象类,抽象方法子类必须重写

抽象方法声明方式:public abstract void work();

抽象类的声明方式:public abstract class Person{...}

抽象类和抽象方法的注意事项:

  • 抽象类不能实例化。

  • 抽象类中不一定有抽象方法(单独利用 abstract 修饰该类),有抽象方法的类一定是抽象类。

  • 可以有构造方法。(目的是为了让子类继承下来之后可以实例化对象)

  • 抽象类的子类:要么重写抽象类中的所有抽象方法,要么子类本身也变成抽象类。

抽象类和抽象方法的意义:在父类使用抽象方法,规定了该方法的方法名和方法返回值等,使得子类的对应方法的方法名和方法返回值得到了统一,防止不同子类在书写时出现混乱无章的现象。

接口

使用场景:一个父类有多个子类,但是其中有一个方法是某些子类没有,但其他子类有的。这个方法抽取到父类显然不太合理,但是如果直接分配到子类当中去的话我们没有办法规定这个方法的写法,就会导致不同的子类对该方法的书写不一致。所以,这个时候我们就需要用到接口。

接口的定义和使用:

  • 接口用关键字 interface 来定义:public interface 接口名 {}

  • 接口中的方法声明:public abstract void 方法名();

  • 接口不能实例化。

  • 接口与类之间是实现关系,通过 implements 关键字表示:public class 类名 implements 接口名 {}

  • 接口的子类(实现类):要么重写接口中的所有抽象方法,要么是抽象类。

  • 接口和类的实现关系,可以单实现,也可以多实现。public class 类名 implements 接口名1,接口名2 {}

  • 实现类还可以在继承一个类的同时实现多个接口。public class 类名 extends 父类 implements 接口名1,接口名2 {}

注意:C++中可以使用多继承,所以没有定义接口的关键字,其接口的实现直接使用一个接口虚基类,然后让子类同时继承父类和接口虚基类即可。而 Java 没办法使用多继承,所以使用 implements 关键字进行接口的继承。

接口中成员的特点:

  • 成员变量:只能是常量,默认修饰符为 public static final

  • 构造方法:接口没有构造方法。

  • 成员方法:JDK7 以前只能使用抽象方法,默认修饰符为 public abstract

  • JDK8 新特性:接口中可以定义有方法体的默认方法,其目的是为了防止当接口新增规则的时候,子类需要去重写全部新增的方法,耗时耗力,不方便。默认方法使用 default 关键字修饰,不是抽象方法,不强制被重写,若要重写,则需要去掉 default。默认方法定义格式:public default 返回值类型 方法名(参数列表) {}。作用是解决接口升级问题。如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写,不重写的话编译器不知道要调用哪个方法。

public interface Inter {
    public abstract void method();
    public default void show() {
        System.out.println("接口中的默认方法");
    }
}
public class InterImpl implements Inter {
    //不需要强制重写default方法
    @Override
    public void method() {
        System.out.println("抽象方法的重写");
    }
}
  • JDK8新特性:接口中可以定义 static 修饰的静态方法。接口中的 static 方法无法被重写,只能通过接口名调用,不能通过实现类名或者对象调用。
public interface Inter {
    public abstract void method();
    public static void show() {
        System.out.println("接口当中的静态方法");
    }
}
public class InterImpl implements Inter {
    //不需要重写static修饰的show方法
    @Override
    public void method() {
        System.out.println("method的重写");
    }
}
public class Test0 {
    public static void main(String[] args) {
        //直接利用接口名调用static方法
        Inter.show();
    }
}
  • JDK9新特性:可以定义私有方法。
public interface Inter {
    //普通私有方法,给默认方法服务的
    private void log() {
        //该方法我不希望被其他东西调用,故设置为私有
        System.out.println("记录运行日志");
    }
    public default void start() {
        System.out.println("start方法执行");
        log();
    }
    public default void end() {
        System.out.println("end方法执行");
        log();
    }

    //静态私有方法,给静态方法服务的
    private static void log2() {
        System.out.println("静态运行日志");
    }
    public static void show() {
        System.out.println("静态方法的执行");
        log2();
    }
}

各种关系:

  • 类和类的关系:继承关系,只能单继承,不能多继承,但是可以多层继承。

  • 类和接口的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。

  • 接口和接口的关系:继承关系,可以单继承,也可以多继承。

适配器设计模式

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

适配器设计模式:解决接口与接口实现类之间的矛盾问题。

假设一个接口中有多个抽象方法:

public interface Inter {
   public abstract void method1();
   public abstract void method2();
   public abstract void method3();
   public abstract void method4();
}

根据实际需求,我只需要使用 method3 就可以了,但是因为接口的语法特点,其他的方法,我们不得不重写。为了解决代码冗长问题,我们便可以使用适配器设计模式

首先新建一个适配器 InterAdapter 类,对接口 Inter 所有的抽象方法进行空实现。同时,该适配器只是方法的空实现,没有创建对象的意义,所以我们直接将其设定为虚基类(abstract),防止实例化对象。

public abstract class InterAdapter implements Inter {
    @Override
    public void method1() { }
    @Override
    public void method2() { }
    @Override
    public void method3() { }
    @Override
    public void method4() { }
}

接下来,创建 InterImpl 类继承 InterAdapter 类,再次重写我们需要用的方法。注意,这里 InterImpl 会继承所有方法,只不过我们不需要用到的方法是空实现而已。

public class InterImpl extends InterAdapter {
    @Override
    public void method3() {
        System.out.println("method3在InterImpl中的重写");
    }
}

该方法可以简化代码。

内部类

类的五大成员:属性、方法、构造方法、代码块、内部类。

所谓内部类,就是在类 A 当中再定义了另一个类 B 。则类 B 是类 A 的内部类。

内部类表示的事物是外部类的一部分,内部类单独出现没有意义。JDK16 之前不能定义静态变量,JDK16 开始才可以在内部类定义静态变量。

内部类的访问特点:

  • 内部类可以直接访问外部类的成员,包括私有。

  • 外部类要访问内部类的成员,必须创建对象。

ArrayList 的迭代器就是一个内部类。

public class Car {    //外部类
    String carName;
    int carAge;
    String carColor;
    public void show() {
        //外部类可以自己用自己的成员变量
        System.out.println(carName);
        //外部类无法使用内部类的成员变量
        //System.out.println(engineName);
        //原因是在创建外部类的时候,我们是没有创建内部类对象的,所以外部类的方法无法调用内部类的成员变量

        //先创建出内部类对象,才可以在外部类调用内部类成员
        Engine e = new Engine();
        System.out.println(e.engineName);
    }
    class Engine {    //内部类
        String engineName;
        int engineAge;
        public void show() {
            //内部类可以自己使用自己的成员变量
            System.out.println(engineName);
            //内部类也可以使用外部类的成员变量
            System.out.println(carName);
        }
    }
}

成员内部类

写在成员位置的,属于外部类的成员。成员内部类可以被一些修饰符所修饰,比如:private,默认,protected,public,static 等。

获取成员内部类对象:

  • 在外部类中编写方法,对外提供内部类对象。

  • 直接创建的格式:外部类名.内部类名 对象名 = new 外部类对象().new 内部类对象();其中,new 外部类对象() 创建了一个外部类对象,然后再用这个外部类对象创建内部类对象,使用了链式编程的思想。

public class Outer {
    private class Inner {}
    public Inner getInstance() {
        return new Inner();
    }
}    
public class Main {
    public static void main(String[] args) {
        Outer o = new Outer();    //创建外部类对象
        o.getInstance();    //外部类对象通过函数创建内部类对象

        Outer.Inner oi = new Outer().new Inner(); //直接创建
    }
}

内部类中,使用 this 关键字指向的是内部类本身,使用外部类.this才是外部类自身。

public class Outer {
    private int a = 10;
    class Inner {
        private int a = 20;
        public void show() {
            int a = 30;
            System.out.println(a); //30
            System.out.println(this.a);//20
            System.out.println(Outer.this.a);//10
        }    
    }
}    

静态内部类

静态内部类只能访问外部类中的静态变量和静态方法。如果想访问非静态的需要创建外部类的对象。

创建静态内部类对象格式:外部类名.内部类名 对象名 = new 外部类名.内部类名();

调用非静态方法的格式:先创建对象,用对象调用。

调用静态方法的格式:外部类名.内部类名.方法名();

public class Outer {

    int a = 10;
    static int b = 20;

    static class Inner {
        public void show1() {
            System.out.println("非静态的方法被调用");
           // System.out.println(a);    错误的,静态内部类只能访问静态变量或者静态方法
            Outer o = new Outer();
            System.out.println(o.a);    //正确的,静态内部类要调用非静态成员或者方法需要先创建外部类对象
            System.out.println(b);
        }

        public static void show2() {
            System.out.println("静态的方法被调用");

        }
    }
}
public class Main {
    public static void main(String[] args) {
        //创建静态内部类对象
        Outer.Inner oi = new Outer.Inner();
        oi.show1();
        oi.show2();//可以这么写,但是java不提倡使用对象调用静态方法
        Outer.Inner.show2();   //比较提倡的,是使用类名调用静态方法
    }
}

局部内部类

将内部类定义在方法里面就叫局部内部类,类似于方法里面的局部变量(可以被 final 修饰,但不能被 public 修饰)。外界是无法直接使用,需要在方法内部创建对象并使用(与外界无法直接使用方法中的局部变量相似)。该类可以直接访问外部类的成员,也可以访问方法内的局部变量。

public class Outer {

    int a = 10;

    public void show() {
        int b = 10; //局部变量

        class Inner {   //局部内部类,与局部变量类似
            String name;
            int age;
            public void method1() {
                System.out.println("局部内部类中的method方法");
                //局部内部类可以直接访问外部类成员和方法内成员
                System.out.println(a);
                System.out.println(b);
            }
            public static void method2() {
                System.out.println("局部内部类中的静态method方法");
            }
        }

        //方法中创建局部内部类的对象
        Inner i = new Inner();
        //方法中有了局部内部类对象之后,可以使用该类的成员变量和方法
        System.out.println(i.name);
        System.out.println(i.age);
        i.method1();
    }
}

匿名内部类

匿名内部类实际上就是隐藏了名字的内部类。new 出来的匿名内部类可看作是自身的子类

格式如下:

new 类名或者接口名 {
    重写方法;
};

匿名内部类接口形式如下:

public interface Swim {
    public abstract void swim();
}
public class Main {
    public static void main(String[] args) {
        new Swim() {    //匿名内部类
            @Override
            public void swim() {
                System.out.println("重写了swim方法");
            }
        };
        /*
        * 把前面的class和类名删掉,剩余的内容就变成一个没有名字的类
        * 这个没有名字的类想要实现swim接口:
        * 1.把Swim写在大括号前面,表示这个没有名字的类实现了Swim接口,故需要重写对应的抽象方法
        * 2.还想要创建这个没有名字的类的对象,就直接插入new ();
        * 整个整体实际是new出来的对象,真正没有名字的类实际是{};当中的内容,包括{};
        * */
    }
}

匿名内部类抽象类形式如下:

public abstract class Animal {
    public abstract void eat();
}
public class Main {
    public static void main(String[] args) {
        new Animal() {
            @Override
            public void eat() {
                System.out.println("重写的eat方法");
            }
        };
    }
}

匿名内部类的使用场景:

  • 当作参数触发多态,方便进行抽象类或者接口的测试。
public class Main {
    public static void main(String[] args) {

        new Animal() {
            @Override
            public void eat() {
                System.out.println("eat方法的重写");
            }
        };

        /*若要测试Animal,以前的方式是创建一个类去继承Animal
        *Dog d = new Dog();
        *method(d);
        *假设我只要进行测试,那么这个Dog类我将会只使用一次,不方便
        * 利用匿名内部类可以解决这个问题,把匿名内部类当作参数传入方法中
        * */

        //匿名内部类当作参数传入method中
        method(
                new Animal() {
                    @Override
                    public void eat() {
                        System.out.println("重写eat方法");
                    }
                }
        );

    }

    public static void method(Animal a) {   //多态 Animal a = new 子类对象
        a.eat();    //编译看左边,运行看右边
    }

}
  • 使用实现类接收,或者直接调用方法。
public class Main {
    public static void main(String[] args) {
        //整体可以看作是swim这个接口的一个实现类
        //接口多态
        Swim s = new Swim() {
            @Override
            public void swim() {
                System.out.println("swim方法的重写");
            }
        };

        s.swim();   //实现类调用函数

        new Swim() {
            @Override
            public void swim() {
                System.out.println("swim方法的调用");
            }
        }.swim();   //匿名内部类直接调用函数

    }
}

常用API

Math

Math是一个帮助我们进行数学计算的工具类。常用方法如下:

方法名 说明
abs(int a) 获取参数绝对值
ceil(double a) 向上取整
floor(double a) 向下取整
round(float a) 四舍五入
max(int a, int b) 获取较大值
pow(double a, double b) a的b次幂
random() 返回值为double的随机值,范围 [ 0.0,1.0 )
sqrt(double a) 返回a的平方根
cbrt(double a) 返回a的立方根

使用的时候要先书写类名,再去调用方法。例如:Math.cbrt(8);

System

System提供了一些与系统相关的方法:

方法名 说明
public static void exit(int status) 终止当前运行的 Java 虚拟机
public static long currentTimeMillis() 返回当前系统的时间毫秒值形式,该方法可以用于比较不同算法的运行时间
public static void arraycopy(数据原数组,起始索引值,目的地数组,起始索引值,拷贝个数) 数组拷贝

计算机的时间原点:1970年1月1日 00:00:00。(C语言的生日)

而中国位于东八区,有8个小时的时差,故中国国区获取的计算机时间原点为:1970年1月1日 08:00:00。

public class Main {
    public static void main(String[] args) {

        long l = System.currentTimeMillis();
        System.out.println(l);

        /*currentTimeMillis()方法可以用于调查算法运行时间,具体操作如下:
        *
        * long start = System.currentTimeMillis();
        * ...具体算法...
        * long end = System.currentTimeMillis();
        * System.out.println(start - end);
        *
        * */


        //拷贝数组
        int[] arr1 = {1,2,3,4,5,6,7,8,9,10};
        int[] arr2 = new int[10];
        int[] arr3 = new int[10];
        //将arr1的内容拷贝到arr2中
        System.arraycopy(arr1, 0, arr2, 0, 10);// 1 2 3 4 5 6 7 8 9 10
        System.arraycopy(arr1, 0, arr3, 4, 3);// 0 0 0 0 1 2 3 0 0 0
        /*
        * 如果数据类型是基本数据类型,就必须保证二者数据类型相等,否则会报错
        * 在拷贝的时候需要考虑数组的长度,如果超出范围,也会报错
        * 如果数据源数组和目的地数组都是引用数据类型,则子类类型可以赋值给父类类型
        * */

        //状态码:0 -> 表示虚拟机正常停止;非0 -> 表示虚拟机异常停止
        System.exit(0);
        System.out.println("查看是否执行");   //不会打印,因为上一行代码已经停止了虚拟机的运行

    }
}

Runtime

Runtime表示当前虚拟机的运行环境。

方法名 说明
public static Runtime getRuntime() 当前系统的运行环境对象
public void exit(int status) 停止虚拟机
public int availableProcessors() 获得CPU的线程数
public long maxMemory() JVM 能从系统中获取总内存大小(单位 byte)
public long totalMemory() JVM 已经从系统中获取总内存大小(单位 byte)
public long freeMemory() JVM 剩余内存大小(单位 byte)
public Process exec(String command) 运行 cmd 命令

Runtime 的实现源代码包括了如下部分:

//final修饰,创建一个地址值不会改变的对象
private static final Runtime currentRuntime = new Runtime();	
public static Runtime getRuntime() {
    return currentRuntime;	//利用getRuntime来得到这个对象
}
private Runtime() {}	//私有构造函数,无法直接调用

其中,Runtime 的构造函数为私有,我们只能通过 getRuntime 这个函数来获取其对象,没办法通过 new 来获取。并且,获取到的对象是唯一的,地址值相同。

System.exit(0);的底层是通过调用 Runtime 的 exit 来实现的:Runtime.getRuntime().exit(0);

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {

        //当前系统的运行环境对象
        Runtime r1 = Runtime.getRuntime();
        Runtime r2 = Runtime.getRuntime();

        System.out.println(r1 == r2);   //打印true,说明两个对象的地址值相同

        //获得CPU的线程数
        System.out.println(Runtime.getRuntime().availableProcessors());

        //JVM能从系统中获取总内存的大小,除以两次1024,将byte转化为mb
        System.out.println(Runtime.getRuntime().maxMemory() / 1024 / 1024);
        //JVM已经获取的内存大小
        System.out.println(Runtime.getRuntime().totalMemory() / 1024 / 1024);
        //JVM剩余内存大小
        System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024);

        //运行cmd命令
        Runtime.getRuntime().exec("notepad");   //打开记事本
        /*
        * shutdown:关机
        * 加上参数才能运行
        * -s:默认1分钟后关机
        * -s -t 指定时间:指定关机时间
        * -a:取消关机操作
        * -r:关机并重启
        * */
     //   Runtime.getRuntime().exec("shutdown -s -t 3600"); 在3600秒后关机
        Runtime.getRuntime().exec("shutdown -a");

        Runtime r3 = Runtime.getRuntime();
        r3.exit(0);  //停止虚拟机
        System.out.println("查询是否执行");
    }
}

Object

Object 是 Java 中的顶级父类。所有的类都直接或者间接地继承于 Object 类。Object 类中的方法可以被所有子类访问。

方法名 说明
public Object() 空参构造
public boolean equals(Object obj) 比较两个对象是否相等
protected Object clone(int a) 对象克隆
public String toString() 返回对象的字符串表示形式

toString 方法:

public class Main {
    public static void main(String[] args) {

        //toString 返回对象的字符串表示形式
        //包名 + 类名 + 地址值
        Object obj = new Object();
        String str1 = obj.toString();
        System.out.println(str1);   //java.lang.Object@723279cf
        System.out.println(obj);    //直接打印也可以获得相同结果
        /*
        * System:类名
        * out:静态变量
        * System.out:获取打印的对象
        * print():方法,会调用toString
        * 参数:表示打印的内容
        * */

        //如果要打印对象属性,可以重写toString方法


    }
}

equals 方法:

假设有一个 Student 类,如下:

import java.util.Objects;

public class Student {
    private String name;
    private int age;

    //IDEA中使用 alt + insert,快捷重写equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true; //同一个对象,直接true
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;  //强转成子类对象
        return age == student.age && Objects.equals(name, student.name);    //进行对比
    }
}

该类对 equals 方法进行了重写,就可以让 equals 对比对象属性值了。

String 类型和 StringBuilder 类型的 equals:

public class Main {
    public static void main(String[] args) {

        String s = "abc";
        StringBuilder sb = new StringBuilder("abc");

        System.out.println(s.equals(sb));   //false
        /*
        * 这个时候是调用的字符串中的equals
        * 字符串中的equals方法是先判断参数是否为字符串
        * 1.如果是字符串,则比较内容
        * 2.如果不是,直接返回false
        * String 和 StringBuilder 不是同一个类型
        * 故最终返回false
        * */

        System.out.println(sb.equals(s));   //false
         /*
         * StringBuilder 内部没有封装equals方法
         * 所以这里的equals实际是Object的equals
         * 默认比较对象的地址值,s和sb地址值不一样,故false
         * */
        
    }
}

clone 方法:

clone 方法在设计的时候是 protected 的,外界无法直接访问,需要子类重写 clone 方法

而后还需要让子类 implements 一个 Cloneable 接口,该接口是空的,属于标记性接口

浅拷贝:直接拷贝地址值,使得一个对象对数据进行修改的时候,另一个对象的数据也可能会被修改。

深拷贝:新开对象存成员变量,不会相互影响,但是 String 类型还是会复用

Object 默认的克隆是浅克隆。

import java.util.Arrays;

public class Student implements Cloneable {
    //Cloneable是没有任何方法的,是标记性接口
    //表示这个接口一旦实现了,表示当前类的对象可以被克隆
    public int age;
    public String name;
    public int[] arr = {1,2,3,4,5};

    public Student() { }

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

    @Override
    public String toString() {
        return "年龄:" + this.age + " 姓名:" + this.name + " 数组:" + Arrays.toString(arr);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //调用父类中的clone方法
        //相当于让Java克隆一个对象,然后返回,返回的是一个Object对象,最后还需要强转
        return super.clone();
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        //先创建对象
        Student stu1 = new Student(18, "小A");

        //克隆对象,需要强转
        Student stu2 =(Student) stu1.clone();
        //一下两个对象属性都相同,克隆成功
        System.out.println(stu1);
        System.out.println(stu2);

        //验证浅克隆
        stu1.arr[0] = 2;
        //数组会跟着改变
        System.out.println(stu1); //2 2 3 4 5
        System.out.println(stu2); //2 2 3 4 5
    }
}

重写深拷贝克隆方法:

import java.util.Arrays;

public class Student implements Cloneable {
    //... 成员变量和getter、setter方法代码省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        int[] data = this.arr;
        int[] newData = new int[data.length];
        for(int i = 0; i < data.length; ++i) {
            newData[i] = data[i];
        }
        Student stu = (Student) super.clone();  //自己创建一个对象
        stu.arr = newData;
        return stu;
    }
}

clone 一般会用第三方工具来帮助我们进行深拷贝。

Objects

Objects 是一个工具类,提供了一些方法去完成一些功能。

方法名 说明
public static boolean equals(Object a, Object b) 先做非空判断,比较两个对象
public static boolean isNull(Object obj) 判断对象是否为null,为null就返回true,反之返回false
public static boolean nonNull(Object obj) 判断对象是否为非null
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        //创建对象
        Student stu1 = null;
        Student stu2 = new Student(18, "张三");

        //比较两个学生是否相同,会先判断stu1是否为null
        boolean result = Objects.equals(stu1, stu2);
        System.out.println(result);

        Student stu3 = new Student();
        Student stu4 = null;
        System.out.println(Objects.isNull(stu3));   //false
        System.out.println(Objects.isNull(stu4));   //true

        System.out.println(Objects.nonNull(stu3));  //true
        System.out.println(Objects.nonNull(stu4));  //false
    }
}

BigInteger

高精度整型,对象一旦创建,内部的值不能改变。使用时需包含import java.math.BigInteger;,构造方法如下:

方法名 说明
public BigInteger(int num, Random rnd) 获取随机大整数,范围:
public BigInteger(String val) 获取指定的大整数
public BigInteger(String val, int radix) 获取指定进制的大整数
public static BigInteger valueOf(long val) 静态方法获取BigInteger对象,内部有优化
import java.math.BigInteger;
import java.util.Random;
import java.util.function.BiFunction;

public class Main {
    public static void main(String[] args) {
        //获取随机大整数
        BigInteger bd1 = new BigInteger(4, new Random());
        System.out.println(bd1);

        //获取一个指定的大整数
        BigInteger bd2 = new BigInteger("100000000000000000000000");
        System.out.println(bd2);

        //获取指定进制的大整数
        BigInteger bd3 = new BigInteger("100", 16);
        System.out.println(bd3);    //打印256,16进制的100等于10进制的256

        //静态方法创建,内部有优化
        /*
        * 1.能表示的范围比较小,只能是long类型之内
        * 2.在内部对常用数字 -16 ~ 16 进行了优化:
        *   提前把 -16 ~ 16 先创建好BigInteger的对象,这样就不会多次创建新的了
        *   超过上述范围的才会去重新new
        * */
        BigInteger bd4 = BigInteger.valueOf(16);
        BigInteger bd5 = BigInteger.valueOf(16);
        System.out.println(bd4 == bd5); //true,地址值相同
        
        BigInteger bigInteger1 = BigInteger.valueOf(17);
        BigInteger bigInteger2 = BigInteger.valueOf(17);
        System.out.println(bigInteger1 == bigInteger2);	//这里为false,地址值不同

        BigInteger bd6 = BigInteger.valueOf(1);
        BigInteger bd7 = BigInteger.valueOf(2);
        BigInteger result = bd6.add(bd7);   //新的对象,不会修改参与计算的对象的值
        System.out.println(result);
    }
}

常见成员方法:

add、subtract(减法)、multiply、divide(除法、获取商)、divideAndRemainder(除法、获取商和余数)、equals、pow、max、min、intValue。

import java.math.BigInteger;

public class Main {
    public static void main(String[] args) {
        //创建两个大整数对象
        BigInteger bd1 = BigInteger.valueOf(15);
        BigInteger bd2 = BigInteger.valueOf(13);

        //加法
        BigInteger add = bd1.add(bd2);
        System.out.println(add);    //28

        //除法,获取除数和商
        BigInteger[] divideAndRemainder = bd1.divideAndRemainder(bd2);
        System.out.println(divideAndRemainder[0]);  //1
        System.out.println(divideAndRemainder[1]);  //2

        //比较是否相同
        BigInteger bd3 = BigInteger.valueOf(13);
        boolean result1 = bd1.equals(bd2);
        boolean result2 = bd3.equals(bd2);
        System.out.println(result1); //false
        System.out.println(result2); //true

        //次幂
        BigInteger pow = bd1.pow(2);
        System.out.println(pow);    //225

    }
}

BigInteger 底层存储方式:

  • 封装了一个 final int signum; ,-1表示负数,0表示零,1表示整数。
  • 封装了数组 final int[] mag;,用于存储数据。首先会将大整数转换为二进制补码,从右往左32位分为一组,然后将每一组的二进制码转为10进制,存入 mag 数组中。

BigDecima

十进制的小数转二进制:假设有一个十进制的小数a,则先让a乘2,取a的整数部分,接下来再取a的小数部分,乘2……重复上述过程,例如:0.875转成二进制就是111。这种方法会使得小数部分转二进制会很长,例如:0.9的二进制是111001100110011001100110011001100110011001101(共45位,float 能存32位,double 能存64位)。但是实际计算机没办法存储这么多的小数位,所以会舍弃掉一部分,故导致了精度上不足。而 Java 当中的 BigDecima 表示了大小数,可以用来进行小数的精确运算。使用时要加上java.math.BigDecimal;

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        double ans = 0.01 + 0.09;
        System.out.println(ans);    //0.09999999999999999,内容不精确

        //传递double类型的小数,但是该方法会有不可预知的错误
        BigDecimal bd1 = new BigDecimal(0.09);
        BigDecimal bd2 = new BigDecimal(0.01);
        System.out.println(bd1);    //0.0899999999999999966693309261245303787291049957275390625
        System.out.println(bd2);    //0.01000000000000000020816681711721685132943093776702880859375

        //字符串构造
        BigDecimal bd3 = new BigDecimal("0.09");
        BigDecimal bd4 = new BigDecimal("0.01");
        System.out.println(bd3);    //0.09
        System.out.println(bd4);    //0.01
        System.out.println(bd3.add(bd4));   //0.10

        //通过静态方法获取对象
        BigDecimal valueOf = BigDecimal.valueOf(10);   //以10.0的方式存储
        System.out.println(valueOf);   //10

        /*
        * 如果存储的数字范围不超过double,建议使用valueOf构造
        * 如果存储的数字范围超过了double,建议使用string来构造
        * 如果传递了 0 ~ 10 之间的数,这些数会被提前创建,不会重复创建
        * */
    }
}

divide 方法:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        //创建对象
        BigDecimal bd1 = BigDecimal.valueOf(10.0);
        BigDecimal bd2 = BigDecimal.valueOf(2.1);
        System.out.println(bd1.add(bd2));   //加法 12.1
        System.out.println(bd1.subtract(bd2));  //减法 7.9
        System.out.println(bd1.multiply(bd2));  //乘法 21.00

        //除法
        //System.out.println(bd1.divide(bd2));  除不尽,会报错
        //divide方法,如果除不尽,需要使用divide的第二形式

        BigDecimal bd3 = bd1.divide(bd2, 2, RoundingMode.HALF_UP);  //HALF_UP -> 四舍五入
        System.out.println(bd3);    //4.76

        /* RoundingMode;
        * UP:远离0
        * DOWN:向0
        * CEILING: 向 +∞
        * FLOOR: 向 -∞
        * HALF_UP: 四舍五入,0.5 入为 1
        * HALF_DOWN: 四舍五入,0.5 入为 0
        * */
    }
}

BigDecima 的底层存储方式:

因为小数的二进制形式很长,就算使用像 BigInteger 一样的分段存储方式的话,也很难存下,故 BigDecima 的存储方式另有设计:BigDecima 会先遍历存进来的字符串,然后将每一个字符转化为ASCII码,例如:12.36,转化为[‘1’, ‘2’, ‘.’, ‘3’, ‘6’],转化为ASCII码就是[49, 50, 46, 51, 54]。如果有负号,也会将负号存入,转换为对应的ASCII码45。

JDK7时间类

全世界的时间,有一个统一的计算标准。

一开始的标准为格林尼治时间或者格林威治时间(Greenwich Mean Time),简称 GMT。其计算核心是:地球自转一天为24小时,太阳直射时为正午12点。

后来,人们发现格林威治时间的误差太大,最大误差曾经达到了16分钟。故在2012年1月,取消使用格林威治时间。现在的时间是标准原子钟提供的。原子钟是利用铯原子的震动的频率计算出来的时间(铯原子震动 9,192,631,770次,认为是1s),我们把这个时间作为世界标准时间(UTC)

而中国的标准时间计算法则是:世界标准时间 + 8小时。(东八区)

Date

Date 类是一个 JDK 写好的 Javabean 类,用来描述时间,精确到毫秒。

利用空参构造创建的对象,默认表示系统当前时间。

利用有参构造创建的对象,表示指定的时间。

import java.net.SocketTimeoutException;
import java.util.Date;
import java.util.Random;

public class Main {
    public static void main(String[] args) {
        //创建对象表示时间
        Date d1 = new Date();
        System.out.println(d1);

        //创建对象表示一个指定时间
        //表示从时间原点开始过了0ms的时间
        Date d2 = new Date(0L);
        System.out.println(d2);//Thu Jan 01 08:00:00 CST 1970

        //setTime 修改时间
        d2.setTime(1000L);
        System.out.println(d2);//Thu Jan 01 08:00:01 CST 1970

        //getTime 获取当前时间的毫秒值
        long time = d2.getTime();
        System.out.println(time);//1000

        //打印时间原点开始一年之后的时间
        Date date = new Date(31536000000L);
        System.out.println(date); //Fri Jan 01 08:00:00 CST 1971

        //定义两个时间对象,判断哪个时间在前,哪个时间在后
        Random r = new Random();

        //创建两个时间对象
        Date date1 = new Date(Math.abs(r.nextInt()));
        Date date2 = new Date(Math.abs(r.nextInt()));
        long time1 = date1.getTime();
        long time2 = date2.getTime();
        System.out.println(date1);
        System.out.println(date2);
        if(time1 > time2) System.out.println("date1 在后面");
        else if(time1 < time2) System.out.println("date2 在后面");
        else System.out.println("两者相等");

    }
}

SimpleDateFormat

该类有两个作用:

  1. 格式化:把时间变成我们喜欢的格式。
  2. 解析:把字符串表示的时间变成 Date 对象。
构造方法 说明
public SimpleDateFormat() 使用默认格式构造
public SimpleDateFormat(String pattern) 使用指定的格式
常用方法 说明
public final String format(Date date) 格式化(日期对象 -> 字符串)
public Date parse(String source) 解析(字符串 -> 日期对象)

格式化的时间形式的常用的模式对应关系如下:

y M d H m s

例如:2023-12-02 21:19:02 即为 yyyy-MM-dd HH:mm:ss

import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    public static void main(String[] args) {
        
        //空参创建对象
        SimpleDateFormat sdf1 = new SimpleDateFormat();
        Date d1 = new Date(0L);
        //利用format进行默认格式化
        String str1 = sdf1.format(d1);
        System.out.println(str1); //1970/1/1 08:00

        //带参构造创建指定格式的对象
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d2 = new Date(0L);
        //利用指定的格式进行格式化
        String str2 = sdf2.format(d2);
        System.out.println(str2);   //1970-01-01 08:00:00
        
    }
}
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    public static void main(String[] args) throws ParseException {
        //定义一个字符串表示时间
        String str = "2023-11-11 11:11:11";
        //利用带参构造创建对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str);
        //打印结果
        System.out.println(date);   //Sat Nov 11 11:11:11 CST 2023
        System.out.println(date.getTime()); //1699672271000

    }
}
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    public static void main(String[] args) throws ParseException {
        
         //需求:将某个日期格式转换成另一个格式
        
        //将一定格式的日期转化为Date
        String str = "2000-11-11";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date d = sdf.parse(str);

        //将Date转化为一定格式的日期
        SimpleDateFormat sdft = new SimpleDateFormat("yyyy年MM月dd日");
        String date = sdft.format(d);
        System.out.println(date);
    }
}

Calendar

Calendar本身是一个抽象类,不能自己创建对象。

需要通过 getInstance 方法来获取当前时间的日历子类对象。

方法名 说明
public final Date getTime() 获取日期对象
public final setTime(Date date) 给日历设置日期对象
public long getTimeInMillis() 拿到时间的毫秒值
public void setTimeInMillis(long millis) 给日历设置时间毫秒值
public int get(int field) 取日历中的某个字段信息
public void set(int field,, int value) 修改日历的某个字段信息
public void add(int field, int amount) 为某个字段增加、减少指定值
import java.util.Calendar;
import java.util.Date;

public class Main {
    public static void main(String[] args) {
        //Calender是一个抽象类,不能直接new,而是通过一个静态方法获取到子类对象
        //底层:根据系统的不同时区,来获取不同对象
        //会把时间中的纪元,年,月,日,时,分,秒,星期等放入一个数组当中
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar);

        //修改日历代表的时间
        Date date = new Date();
        calendar.setTime(date);
        //用Calender的时候,月份是0~11
        // 而周日是一周的第一天,故星期中,1是周日,2是周一...
        System.out.println(calendar);

        //获取日期中某个字段的信息
        //0-纪元 1-年 2-月 3-一年中的第几周 4-一个月中的第几周 5-一个月中的第几天
        //也可以直接用大写的单词来表示字段
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DATE);
        System.out.println(year + " " + (month+1) + " " + day);
        int week = calendar.get(Calendar.DAY_OF_WEEK);
        System.out.println(getChineseWeek(week));

        //修改字段
        calendar.set(Calendar.YEAR, 2024);
        System.out.println(calendar.get(Calendar.YEAR));
        calendar.set(Calendar.MONTH, 11);   //修改月份为11,因为Calender类的月份是从0开始数,到11结束
        System.out.println(calendar.get(Calendar.MONTH));

        //增加
        calendar.add(Calendar.YEAR, 1);
        System.out.println(calendar.get(Calendar.YEAR));

    }

    //查表法
    public static String getChineseWeek(int index) {
        String[] week = {"", "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
        return week[index];
    }
}

JDK8新增的时间类

代码层面:JDK7 的代码麻烦,要比较时间的话需要将日期对象转换成毫秒值计算,比较麻烦。但是 JDK8 之后判断时间的方法就很简单。

安全层面:JDK7 在多线程环境下会导致数据安全的问题。而 JDK8 的时间日期对象是不可变的,解决了这个问题。

Date类

ZoneId 时区

时区举例:Asia / Shanghai

方法名 说明
static Set< String > getAvailableZoneIds() 获取 Java 中支持的所有时区
static ZoneId systemDefault() 获取系统默认时区
static ZoneId of(String zoneId) 获取一个指定时区
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        //获取所有时区名称
        Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        System.out.println(availableZoneIds.size());    //603
        //打印所有时区信息
        for(String str : availableZoneIds) {
            System.out.println(str);
        }

        //获取系统默认时区
        ZoneId zoneId = ZoneId.systemDefault();
        System.out.println(zoneId);

        //获取指定时区
        ZoneId zoneId1 = ZoneId.of("Asia/Taipei");
        System.out.println(zoneId1);
    }
}
Instant 时间戳
方法名 说明
static Instant now() 获取当前时间的Instant对象(标准时间,北京时间要在此基础上加8个小时)
static Instant ofXxx(long epochMilli) 根据(秒、毫秒、纳秒)获取 Instant 对象
ZoneDateTime atZone(ZoneId zone) 指定时区
boolean isXxx(Instant otherInstant) 判断
Instant minusXxx(long millis) 减少时间
Instant plusXxx(long millis) 增加时间
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        //获取当前时间的Instant对象
        Instant now = Instant.now();
        System.out.println(now);

        //根据秒、毫秒、纳秒获取对象
        Instant instant1 = Instant.ofEpochMilli(0L);
        System.out.println(instant1);    //1970-01-01T00:00:00Z

        Instant instant2 = Instant.ofEpochSecond(1L);
        System.out.println(instant2);   //1970-01-01T00:00:01Z

        Instant instant3 = Instant.ofEpochSecond(1L, 1000000000L);  //1s + 1000000000ns = 2s
        System.out.println(instant3);   //1970-01-01T00:00:02Z

        //指定时区
        ZonedDateTime zonedDateTime = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime);  //2023-12-09T16:19:55.379594800+08:00[Asia/Shanghai]

        //is判断
        Instant instant4 = Instant.ofEpochMilli(0L);
        Instant instant5 = Instant.ofEpochMilli(10L);
        System.out.println(instant4.isBefore(instant5));    //true
        System.out.println(instant4.isAfter(instant5));     //false


        //减少时间
        Instant instant6 = Instant.ofEpochMilli(34215462357L);
        System.out.println(instant6);   //1971-02-01T00:17:42.357Z
        System.out.println(instant6.minusSeconds(42L)); //1971-02-01T00:17:00.357Z
    }
}
ZoneDateTime 带时区的时间
方法名 说明
static ZoneDateTime now() 获取当前时间
static ZoneDateTime ofXxx(...) 获取指定时间
ZoneDateTime withXxx(...) 修改时间
ZoneDateTime minusXxx(...) 减少时间
ZoneDateTime plusXxx(...) 增加时间
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        //获取当前时间
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println(zonedDateTime);  //2023-12-09T16:35:57.525827900+08:00[Asia/Shanghai]

        //获取指定时间对象
        ZonedDateTime zonedDateTime1 = ZonedDateTime.of(2023, 12, 9, 16, 40, 23, 22, ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime1);

        //instant获取对象
        ZonedDateTime zonedDateTime2 = Instant.now().atZone(ZoneId.systemDefault());
        System.out.println(zonedDateTime2);

        //instant 和 zoneid 一起创建对象
        Instant instant = Instant.now();
        ZoneId zoneId = ZoneId.of("Asia/Shanghai");
        ZonedDateTime zonedDateTime3 = ZonedDateTime.ofInstant(instant, zoneId);
        System.out.println(zonedDateTime3); //2023-12-09T17:07:53.219199700+08:00[Asia/Shanghai]

        //withXxx修改时间
        ZonedDateTime zonedDateTime4 = zonedDateTime3.withYear(2024);
        System.out.println(zonedDateTime4); //2024-12-09T17:06:35.255891600+08:00[Asia/Shanghai]

        //减少时间
        ZonedDateTime zonedDateTime5 = zonedDateTime3.minusYears(1);
        System.out.println(zonedDateTime5); //2022-12-09T17:07:53.219199700+08:00[Asia/Shanghai]

        //增加时间
        ZonedDateTime zonedDateTime6 = zonedDateTime3.plusMonths(1);
        System.out.println(zonedDateTime6); //2024-01-09T17:11:33.852547100+08:00[Asia/Shanghai]

        //JDK8新增的时间对象是不可变的,如果增加或减少了时间,会产生一个新的时间
    }
}

DateTimeFormatter

方法名 说明
static DateTimeFormatter ofPattern(格式) 获取格式对象
String format(时间对象) 按照指定方式格式化
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    public static void main(String[] args) {
        //获取时间对象
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println(zonedDateTime);  //2023-12-09T17:18:05.282289200+08:00[Asia/Shanghai]

        //解析、格式化
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println(dateTimeFormatter.format(zonedDateTime));    //2023-12-09 17:19:22
    }
}

日历类Calendar

LocalDate(年月日)、LocalTime(时分秒)、LocalDateTime(年月日时分秒)

方法名 说明
static xxx now() 获取当前时间对象
static xxx of (...) 获取指定时间对象
get开头 获取日历中的年月日时分秒等信息
isBefore、isAfter 比较两个 LocalDate
with开头 修改时间
minus开头 减少时间
plus开头 增加时间
isLeapYear 判断是否为闰年

此外,LocalDateTime 还可以转化成 LocalDate 和 LocalTime。

方法名 说明
public LocalDate toLocalDate() 转成 LocalDate
public LocalTime toLocalTime() 转成 LocalTime
import java.time.*;

public class Main {
    public static void main(String[] args) {

        //获取本地日历对象
        LocalDate localDate = LocalDate.now();
        System.out.println(localDate);  //2023-12-09

        //获取指定日历对象
        LocalDate localDate1 = LocalDate.of(2024, 1, 1);
        System.out.println(localDate1); //2024-01-01

        //获取年月日
        int year = localDate.getYear();
        int month = localDate.getMonthValue();
        int day = localDate.getDayOfMonth();
        System.out.println(year + " " + month + " " + day); //2023 12 9
        //获取月份的第二种方式
        Month month1 = localDate.getMonth();
        System.out.println(month1); //DECEMBER
        System.out.println(month1.getValue());  //12

        //is minus plus 等方法不做演示

    }
}
import java.time.*;

public class Main {
    public static void main(String[] args) {

        //判断今天是否是你的生日
        LocalDate birDate = LocalDate.of(2004, 8, 9);
        //获取月份日期
        MonthDay birMonthDay = MonthDay.of(birDate.getMonthValue(), birDate.getDayOfMonth());
        MonthDay localMonthDay = MonthDay.now();

        System.out.println(birMonthDay.equals(localMonthDay));

    }
}

工具类

用于计算时间间隔:Duration(秒、纳秒)、Period(年、月、日)、ChronoUnit(所有单位,较常用)

import java.time.*;

public class Main {
    public static void main(String[] args) {
        //计算年月日的时间间隔
        LocalDate localDate = LocalDate.now();
        LocalDate localDate1 = LocalDate.of(2022, 12, 28);

        //使用between方法,计算时间差
        Period period = Period.between(localDate1, localDate);  //第二个参数减掉第一个参数
        System.out.println(period);
        //获取年、月、日
        System.out.println(period.getYears());
        System.out.println(period.getMonths());
        System.out.println(period.getDays());
        //计算总月份
        System.out.println(period.toTotalMonths());
        
    }
}
import java.time.*;

public class Main {
    public static void main(String[] args) {
        //本地日期对象
        LocalDateTime localDateTime = LocalDateTime.now();
        //出生日期对象
        LocalDateTime localDateTime1 = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
        Duration duration = Duration.between(localDateTime1, localDateTime);
        //打印时间对象
        System.out.println(duration);

        //获取相差天数,小时数,分钟数,毫秒数,纳秒数等
        System.out.println(duration.toDays());
        System.out.println(duration.toHours());
        System.out.println(duration.toMinutes());
        System.out.println(duration.toMillis());
        System.out.println(duration.toNanos());
    }
}
import java.time.*;
import java.time.temporal.ChronoUnit;

public class Main {
    public static void main(String[] args) {
        LocalDateTime today = LocalDateTime.now();
        LocalDateTime birDate = LocalDateTime.of(2004, 8, 9, 10, 20, 1);

        //相差的年数
        long years = ChronoUnit.YEARS.between(birDate, today);
        //相差的月数
        long months = ChronoUnit.MONTHS.between(birDate, today);
        //相差的天数
        long days = ChronoUnit.DAYS.between(birDate, today);
        //相差的时数
        long hours = ChronoUnit.HOURS.between(birDate, today);
        //相差的分数
        long minutes = ChronoUnit.MINUTES.between(birDate, today);
        //相差的秒数
        long seconds = ChronoUnit.SECONDS.between(birDate, today);
        //相差的毫秒数
        long mills = ChronoUnit.MILLIS.between(birDate, today);
        //相差的纳秒数
        long nanos = ChronoUnit.NANOS.between(birDate, today);
        //相差的半天数
        long halfDay = ChronoUnit.HALF_DAYS.between(birDate, today);
        //相差的十年数
        long decade = ChronoUnit.DECADES.between(birDate, today);
        //相差的百年(世纪)数
        long centuries = ChronoUnit.CENTURIES.between(birDate, today);
        //相差的千年数
        long millennia = ChronoUnit.MILLENNIA.between(birDate, today);
        //相差的纪元数
        long eras = ChronoUnit.ERAS.between(birDate, today);
    }
}

正则表达式

正则表达式可以校验字符串是否满足一定的规则,并用来校验数据格式的合法性。

例子如下:

public class Main {
    public static void main(String[] args) {
        /*
        * 校验qq号
        * 1.位数必须在6 - 20位
        * 2.开头不能为0
        * 3.必须全是数字
        * */

        String qq1 = "123456789";
        boolean matches1 = qq1.matches("[1-9]\\d{5,19}");
        System.out.println(matches1);    //true
        String qq2 = "123456789a";
        boolean matches2 = qq2.matches("[1-9]\\d{5,19}");
        System.out.println(matches2);   //false

    }
}

正则表达式有两个作用:

  1. 校验字符串是否满足规则。
  2. 在一段文本中查找满足要求的内容。

校验字符串是否满足规则

字符类(只匹配一个字符

取并集不用写特殊字符,直接写在一起就好,也可以用括号区分;取交集用&&;取反用^,取或用 | ,但是取或的时候注意需要加小括号进行分组,否则会出现逻辑混乱。

表达式 规则
[abc] 只能是a,b,c
[^abc] 除了a,b,c之外的任何字符
[a-zA-Z] a到z,A到Z
[a-d[m-p]] a到d,或m到p
[a-z&&[def]] a-z和def的交集,为:d,e,f
[a-z&&[^bc]] a-z和非bc的交集,等同于:[ad-z]
[a-z&&[^m-p]] a到z和除了m到p的交集,等同于:[a-lq-z]

"ab".matches("[abc]");返回的是fasle,因为只能匹配一个字符。

"&".matches("[a-z&[def]]");返回的是true,因为正则式当中的&符号不具有交集含义,只是一个&符号。

预定义字符(只匹配一个字符

表达式 规则
. 任意字符
\d 任意数字(等价于[1-9]
\D 任意非数字(等价于[^1-9]
\s 一个空白字符(等价于[\t\n\x0B\f\r]
\S 非空白字符
\w 英文、数字、下划线
\W 非英文、数字、下划线

表达式中有\号,是普通的字符,但是 Java 里面\还有特殊含义,所以再加一个\使得第二个\起到正常的作用。

数量词(匹配多个字符)

表达式 规则
X? X出现一次或零次
X* X出现零次或多次
X+ X出现一次或多次
X{n} X正好出现 n 次
X{n,} X至少出现 n 次
X{n,m} X至少出现 n 次但不超过 m 次
(?i)X 忽略X的大小写
X?=Y 匹配的时候按XY匹配,=表示X后在匹配时要跟随的数据,但实际截取时截取?前的内容
x?:Y 匹配的时候按XY匹配,实际截取XY
x!:Y 匹配时排除X后跟Y的情况

在一段文本中查找满足要求的内容

本地爬虫

有如下文本,需要找出“Javaxx”的内容。

文本如下:Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个版本是长期支持的版本,下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。

这个时候,可以使用 Pattern 和 Matcher 进行爬取。其中,Pattern 是正则表达式,Matcher 是文本匹配器。使用时需要import java.util.regex.Pattern;import java.util.regex.Mathcher;

import  java.util.regex.Pattern;
import  java.util.regex.Matcher;

public class Main {
    public static void main(String[] args) {
       String str = "Java自从95年问世以来,经历了很多版本," +
                "目前企业中用的最多的是Java8和Java11,因为这两个版本是长期支持的版本," +
                "下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。";

        //Pattern:正则表达式
        //Matcher:文本匹配器,从字符串头部开始,查找与正则表达式相匹配的内容。

        //testMethod(str);

        Pattern p = Pattern.compile("Java\\d{0,2}");
        Matcher m = p.matcher(str);
        while(m.find()) {
            String ans = m.group();
            System.out.println(ans);
        }
    }

    public static void testMethod(String str) {
        //获取正则表达式对象
        Pattern p = Pattern.compile("Java\\d{0,2}");
        //获取文本匹配器对象m, str是大串, p是规则
        Matcher m = p.matcher(str);
        //拿文本匹配器从头开始读取,寻找是否有满足规则的子串,没有返回false;有就返回true,并返回子串起始索引和末尾索引+1
        boolean b = m.find();
        String s1 = m.group();  //根据find方法记录的索引进行截取
        System.out.println(s1);

        //第二次调用find方法的时候,会继续索引后面的内容
        boolean b1 = m.find();
        String s2 = m.group();  //第二次调用group方法的时候,会再次截取子串,并返回
        System.out.println(s2);
    }
}
网络爬虫

在指定网址爬取指定文本。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class Main {
    public static void main(String[] args) throws IOException {
        //创建URL网址对象
        URL url = new URL("https://blog.hnuxcc21.cn");
        //打开网址连接
        URLConnection conn = url.openConnection();
        //创建一个对象去读取网络中的数据
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line;
        //获取正则表达式对象
        Pattern p = Pattern.compile("字符串");
        //在读取的时候一次性读一整行
        while((line = br.readLine()) != null) {
            //创建文本匹配器对象捕捉内容
            Matcher m = p.matcher(line);
            while(m.find()) {
                System.out.println(m.group());
            }
        }
        br.close();
    }
}
综合练习

需求:把下面文本中的电话,邮箱,手机号,热线都爬出来。

文本: 来黑马程序员学习Java

​ 电话:18512516758,18512508907

​ 或者联系邮箱:boniu@itcast.cn

​ 座机电话:01036517895,010-98951256

​ 邮箱:bozai@itcast.cn

​ 热线电话:400-618-9090,400-618-4000,4006184000,4006189090

代码如下:

import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Main {
    public static void main(String[] args) {

        String str = "来黑马程序员学习Java" +
                "电话:18512516758,18512508907" +
                "或者联系邮箱:boniu@itcast.cn" +
                "座机电话:01036517895,010-98951256" +
                "邮箱:bozai@itcast.cn" +
                "热线电话:400-618-9090,400-618-4000,4006184000,4006189090";

        /*
        * 手机号的正则表达式:1[3-9]\\d{9}
        * 邮箱的正则表达式:\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2}
        * 座机电话的正则表达式:0\\d{2,3}-?[1-9]\\d{4,9}
        * 热线电话的正则表达式:400-?[1-9]\\d{2}-?[1-9]\\d{3}
        * */

        //四个正则表达式利用 | 连接起来
        String regex = "(1[3-9]\\d{9})|(\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2})|" +
                "(0\\d{2,3}-?[1-9]\\d{4,9})|(400-?[1-9]\\d{2}-?[1-9]\\d{3})";

        //获取正则表达式对象,定义规则
        Pattern p = Pattern.compile(regex);
        //获取文本匹配器对象,根据规则p捕捉str的内容
        Matcher m = p.matcher(str);
        //爬取内容
        while(m.find()) {
            String result = m.group();
            System.out.println(result);
        }
    }
}

正则表达式在字符串方法中的使用

方法名 说明
public String[] matches(String regex) 判断字符串是否满足正则表达式的规则
public String replaceAll(String regex, String newStr) 按照正则表达式的规则进行替换
public String[] split(String regex) 按照正则表达式的规则切割字符串

有一段文本如下:测试文本testWords123试文本测testWords456文本测试

现有如下需求:

  1. 把字符串中字母数字替换为vs。
  2. 把中文取出。
public class Main {

    public static void main(String[] args) {
        String str = "测试文本testWords123试文本测testWords456文本测试";

        //将字母数字替换为vs
        String res1 = str.replaceAll("[\\w&&[^_]]+", "vs");
        System.out.println(res1);    //测试文本vs试文本测vs文本测试
        /*
        * replaceAll会创建文本解析器对象
        * 然后从头开始读取字符串中的内容,只要有满足的,那么用第二个参数去替换
        * */

        //将中文分割出来
        String[] split = str.split("[\\w&&[^_]]+");
        for (String s : split) {
            System.out.println(s);
        }


    }
}

捕获分组和非捕获分组

分组就是小括号。每组是有组号的,也就是序号。在正则表达式内部利用 \\组号来表示使用第几个分组的内容,在正则表达式外部利用$组号来表示使用第几个分组的内容。

规则如下:

  1. 从1开始,连续不间断。
  2. 以左括号为基准,最左边的是第一组,其次是第二组,以此类推。

捕获分组就是把这一组数据捕获出来,再用一次。

需求:

  1. 判断一个字符串的开始字符和结束字符是否一致,只考虑一个字符。
  2. 判断一个字符的开始部分和结束部分是否一致,可以有多个字符。
  3. 判断一个字符串的开始部分和结束部分是否一致,开始部分内部每个字符也需要一致。
  4. 将“我我要要要学学学学编编编程程程程”转换为“我要学编程”。
public class Main {

    public static void main(String[] args) {
        //判断一个字符的开始和结束是否一致,只考虑一个字符
        //true:a123a    false:a123b
        String regex1 = "(.).+\\1"; //其中,\\1表示把第一组的数据拿出来再用一次
        System.out.println("a123a".matches(regex1));    //true
        System.out.println("b456b".matches(regex1));    //true
        System.out.println("a123b".matches(regex1));    //false
        System.out.println("-----");

        //判断一个字符串的开始和结束是否一致,考虑多个字符
        //true:abc123abc    false:abc123abd
        String regex2 = "(.+).+\\1";
        System.out.println("abc123abc".matches(regex2));    //true
        System.out.println("b456b".matches(regex2));    //true
        System.out.println("12344".matches(regex2));    //false
        System.out.println("abc123abd".matches(regex2));    //false
        System.out.println("-----");

        //判断一个字符串的开始部分和结束部分是否一致,开始部分内部每个字符也需要一致。
        //true:aaa123aaa false:abcdefg
        //注意组号,是以左括号为基准
        String regex3 = "((.)\\2*).+\\1";
        System.out.println("aaa123aaa".matches(regex3));    //true
        System.out.println("abcdefg".matches(regex3));  //false
        System.out.println("abca".matches(regex3));     //true
        System.out.println("-----");

        //字符串的口吃替换
        String str = "我我要要要学学学学编编编程程程程";
        //把重复的内容替换为单个
        String res = str.replaceAll("(.)\\1+", "$1");
        System.out.println(res);
    }

}

非捕获分组就是仅仅把数据括起来,分组之后不需要再使用本组数据,且不占用组号

符号 含义 举例
(?:正则) 获取所有 Java(?:8|11|17)
(?=正则) 获取前面部分 Java(?=8|11|17)
(?!正则) 获取不是指定内容的前面部分 Java(?!8|11|17)

爬取

带条件爬取

有如下文本:Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个版本是长期支持的版本,下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。

现要有要求地进行爬取:

  1. 爬取版本号为8,11,17的 Java 文本,但是只要 Java,不显示版本号。
  2. 爬取版本号为8,11,17的 Java 文本。
  3. 爬取除了版本号为8,11,17的 Java 文本。
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {

        String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11" +
                "因为这两个版本是长期支持的版本,下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。";


        //需求一:

        //定义正则表达式
        String regex1 = "Java(?=8|11|17)";
        //?表示前面的数据,在这里?表示Java
        //=表示要跟随的数据
        //但是在获取的时候,只获取?前的部分
        Pattern p1 = Pattern.compile(regex1);
        Matcher m1 = p1.matcher(str);
        while(m1.find()) {
            String result = m1.group();
            System.out.println(result);
        }

        //需求二:
        String regex2 = "Java(8|11|17)";
        //String regex2 = "Java(?:8|11|17)";
        Pattern p2 = Pattern.compile(regex2);
        Matcher m2 = p2.matcher(str);
        while(m2.find()) {
            System.out.println(m2.group());
        }

        //需求三:
        String regex3 = "Java(?!8|11|17)";
        Pattern p3 = Pattern.compile(regex3);
        Matcher m3 = p3.matcher(str);
        while(m3.find()) {
            System.out.println(m3.group());
        }
    }
}

贪婪爬取

贪婪爬取:在爬取数据的时候尽可能的多获取数据。(Java 中默认的就是贪婪爬取)

非贪婪爬取:在爬取数据的时候尽可能的少获取数据。(如果我们在数量词+ * 后面加上问号,那么此时就是非贪婪爬取)

有如下文本:Java自从95年问世以来,abbbbbbbbbbaaaaaaaaaaaaaaaaaaa经历了很多版本,目前企业中用的最多的是Java8和Java11,因为这两个版本是长期支持的版本,下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。

现有要求地进行爬取:

  1. 按照ab+的方式爬取ab,b尽可能的多取。
  2. 按照ab+的方式爬取ab,b尽可能的少取。
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws IOException {
        String str = "Java自从95年问世以来,abbbbbbbbbbaaaaaaaaaaaaaaaaaaa经历了很多版本," +
                "目前企业中用的最多的是Java8和Java11,因为这两个版本是长期支持的版本," +
                "下一个长期支持的版本是Java17,相信在未来不久,Java17也会逐渐登上历史舞台。";

        String regex1 = "ab+";  //贪婪爬取
        Pattern p1 = Pattern.compile(regex1);
        Matcher m1 = p1.matcher(str);

        while(m1.find()) {
            String res = m1.group();
            System.out.println(res);    //abbbbbbbbbb
        }

        String regex2 = "ab+?"; //非贪婪爬取
        Pattern p2 = Pattern.compile(regex2);
        Matcher m2 = p2.matcher(str);

        while(m2.find()) {
            String res = m2.group();
            System.out.println(res);    //ab
        }

    }
}

常见算法

本章节主要对这些常见算法做一个简要介绍,算法的具体内容可移步博客的其他文章。

基本查找:直接对容器进行遍历,O(n) 复杂度找到要找的元素。

二分查找:保证数据有序的前提下,折半查找,O(logn) 复杂度找到要找的元素。详情见:二分

插值查找:是二分查找的一个优化方案,具体表现为利用数据分布比例对 mid 的计算进行优化。被优化后的 mid 值为:
$$
mid = min + \frac{key-arr[min]}{arr[max] - arr[min]} \times (max-min)
$$
不过,因为插值查找的本质是利用数据分布比例进行优化,所以插值查找要求数据分布均匀,如果数据分布不均匀,则会导致 mid 的计算误差过大,反而会降低效率。

斐波那契查找:同样也是二分查找的一个优化方案,具体表现为利用黄金分割点对 mid 的计算进行优化。被优化后的 mid 值为:
$$
mid = min + 黄金分割点左半边长度 -1
$$
分块查找:适用于部分有序的查找,分块后,块内无序,但是总体有序。分块有两个原则:

  1. 前一块的最大数据,小于后一块中的所有的数据。
  2. 块数一般等于数字总数的开根号。

可以利用二分先查出块的编号,然后再用基本查找找出具体索引。

扩展的分块查找:当数据无法满足分块查找的要求时,我们需要对分块查找进行修改。主要操作是去掉块与块之间的有序性,即只能使用基本查找找出块的编号,再用基本查找找出具体索引。

排序:排序算法这里不多做赘述。

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 O(n²) O(n) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(n²) O(1) 稳定
插入排序 O(n²) O(n) O(n²) O(1) 稳定
希尔排序 O(nlogn) ~ O(n²) O(n^1.3) O(n²) O(1) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
快速排序 O(nlogn) O(nlogn) O(n²) O(logn) ~ O(n) 不稳定

常见的算法API——Arrays

这是一个操作数组的工具类。

方法名 说明
public static String toString(数组) 把数组拼接成一个字符串
public static int binarySearch(数组,查找的元素) 二分查找查找元素
public static int[] copyOf(原数组,新数组长度) 拷贝数组
public static int[] copyOfRange(原数组,起始索引,结束索引) 指定范围的拷贝数组
public static void fill(数组,元素) 填充数组
public static void sort(数组) 按照默认方式进行数组排序
public static void sort(数组,排序规则) 按照指定的规则排序
import java.util.Arrays;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        //将数组变成字符串
        int[] arr = {1, 2, 3, 4, 5};
        String result = Arrays.toString(arr);
        System.out.println(result); //[1, 2, 3, 4, 5]



        //二分查找查找元素,保证数组中的元素必须有序
        //如果元素存在,则直接返回索引
        //如果元素不存在,则返回 -1*(插入点) - 1
        //考虑到如果我要查找数字0,如果插入到-1*(插入点)的位置,则会导致插入位置冲突,所以后续又-1
        int[] arr1 = {1, 4, 7, 10, 20};
        System.out.println(Arrays.binarySearch(arr1, 4));   //1
        System.out.println(Arrays.binarySearch(arr1, 5));   //-3


        //拷贝数组,底层用arrayCopy实现
        int[] arr2 = Arrays.copyOf(arr1, 5);
        System.out.println(Arrays.toString(arr2));  //[1, 4, 7, 10, 20]
        int[] arr3 = Arrays.copyOf(arr1, 10);
        System.out.println(Arrays.toString(arr3));  //[1, 4, 7, 10, 20, 0, 0, 0, 0, 0]
        int[] arr4 = Arrays.copyOf(arr1, 3);
        System.out.println(Arrays.toString(arr4));  //[1, 4, 7]


        //指定范围拷贝数组,区间是左闭右开
        int[] arr5 = Arrays.copyOfRange(arr1, 0, 4);
        System.out.println(Arrays.toString(arr5));  //[1, 4, 7, 10]


        //排序,默认为升序
        int[] arr6 = {1, 19, 22, 14, 16, 71, 23};
        Arrays.sort(arr6);
        System.out.println(Arrays.toString(arr6));  //[1, 14, 16, 19, 22, 23, 71]


        //排序,降序排列
        Integer[] arr7 = {1, 19, 22, 14, 16, 71, 23};
        Arrays.sort(arr7, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(arr7));  //[71, 23, 22, 19, 16, 14, 1]

        
        //填充
        int[] arr8 = new int[10];
        Arrays.fill(arr8, 2);
        System.out.println(Arrays.toString(arr8));  //[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
        
    }
}

注意:java 中的 sort 方法底层封装了三种排序方式,当数据量在 [0,47) 时,采用插入排序,[47,286)时,采用双轴快速排序,大于等于286时采用归并排序。但如果数据量大于286的情况下但是数据不是高度结构化的话,还是采用双轴快速排序。

Lambda表达式

用于简化匿名内部类的书写。

java 中是面向对象编程的,即先找对象,再让对象做事情。但是我们书写匿名内部类的时候,实际上更看重的是方法体,而非那个匿名的对象。故 Lambda 表达式要做的,是简化这种书写方法。利用的是函数式编程(Functional Programming)的编程思想。

Lambda 表达式是 JDK8 开始后的一种新语法形式。基础语法为:()->{ },其中,小括号对应着方法的形参,->是固定格式,{ }对应着方法的方法体。

注意点

  • Lambda 表达式可以用来简化匿名内部类的书写。
  • Lambda 表达式只能简化函数式接口的匿名内部类的写法。
  • 函数式接口:有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加 @FunctionalInterface 注解。
//@FunctionalInterface
interface Swim {
    public abstract  void swimming();
}

public class Main {
    public static void main(String[] args) {
        //利用匿名内部类的形式调用
        method(new Swim() {
            @Override
            public void swimming() {
                System.out.println("匿名内部类调用swimming");
            }
        });


        //利用Lambda表达式
        method(
                ()->{
                    System.out.println("Lambda表达式调用swimming");
                }
        );


    }

    public static void method(Swim s) {
        s.swimming();
    }
}

Lambda表达式在此基础上还可以进行省略,其核心为:可推导,可省略。即可以被推导出来的东西,就可以省略不写。

Lambda的省略规则:

  1. 参数类型可以省略不写。
  2. 如果只有一个参数,参数类型可以省略,同时()也可以省略。
  3. 如果Lambda表达式的方法体只有一行,大括号,分号,return 都可以省略不写,但需要同时省略。
import java.util.Arrays;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        Integer[] arr = {1, 7, 4, 8, 3, 6, 5, 2};

        //利用Comparator进行排序
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });

        System.out.println(Arrays.toString(arr));

        //利用Lambda表达式简化,使用完整格式改写
        Arrays.sort(arr, (Integer o1, Integer o2)->{
                return o2 - o1;
            }
        );

        //Lambda表达式的省略写法改写,省略参数类型
        Arrays.sort(arr, (o1, o2)->{
                    return o2 - o1;
                }
        );

        //Lambda表达式的省略写法改写,省略大括号分号和return
        Arrays.sort(arr, (o1, o2)-> o2 - o1);

    }
}
import java.util.Arrays;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        //要求:按照字符串的长度进行升序排序
        String[] arr = {"a", "aaaa", "aaa", "aa"};

        //匿名内部类格式
        Arrays.sort(arr, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        });

        //Lambda表达式完整格式
        Arrays.sort(arr, (String o1, String o2)->{
            return o1.length() - o2.length();
        });

        //Lambda表达式省略格式
        Arrays.sort(arr, (o1, o2) -> o1.length() - o2.length());


        System.out.println(Arrays.toString(arr));
    }
}

泛型

JDK5 引入的新特性,可以在编译阶段约束操作的数据类型,并进行检查。泛型只能支持引用数据类型。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;

class Student {
    int age;
    String name;
}
public class Main {
    public static void main(String[] args) {
        //没有泛型的时候,集合可以存储任意的数据,处理为Object类型
        ArrayList list = new ArrayList();
        list.add(123);
        list.add("aaa");
        list.add(new Student());

        //但是无法调用数据类型的特有方法
        ListIterator listIterator = list.listIterator();
        while(listIterator.hasNext()) {
            Object object = listIterator.next();
            System.out.println(object);
        }

    }
}

泛型的好处:把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段就能确定下来。

Java 中的泛型是伪泛型,只在编译时期有效。编译成 class 字节码文件的时候,泛型会消失,称为泛型的擦除。也就是说,当数据类型插入到容器时,这个时候会触发数据类型的检查,看看是否符合泛型要求。但当数据丢到容器里面后,容器是把它按照 Object 类型处理,等到要使用的时候才强转成相对应的泛型。主要原因是为了向下兼容,因为老版本没有泛型,那个时候的容器就是按照 Object 来处理的。所以为了让数据类型转换成 Object 类型,Java 的语法这才要求传入引用类型,这才有了包装类

在指定泛型的具体类型之后,传递数据时可以向其中传入子类类型

泛型类

当一个类中,某个变量的数据类型不确定时,可以定义泛型类。基本语法如下:

修饰符 class 类名<类型> {
    
}

//举例:
public class MyArrayList<E>{
    Object[] obj = new Object[10];
    int size;

    public boolean add(E e) {
        obj[size] = e;
        size++;
        return true;
    }

    public E get(int index) {
        return (E)obj[index];   //需要强转
    }
}

泛型方法

当方法中形参类型不确定时,除了可以使用类名后面定义的泛型,也可以在方法申明上定义自己的泛型。基本语法如下:

修饰符 <类型> 返回值类型 方法名(类型 变量名){

}

//举例:
public static <E> void method(E e) {
    System.out.println(e.toString());
}

public static <E> void method1(E...e) {	//可变参数,可以传入不定的参数个数
    //利用增强for遍历
    for(E element : e) {
        //...
    }
}

泛型接口

定义泛型接口有两种方法,一种是实现类给出具体类型,另一种是实现类延续泛型,创建对象时再确定。基本语法:

修饰符 interface 接口名<类型> {

}

//泛型接口使用方式举例:
//实现类给出具体泛型
public class MyArrayList implements List<String>{
    //重写List的方法
}
MyArrayList list = new MyArrayList();	//使用的时候就不需要指定类型了

//实现类延续泛型
//下方第一个E是实现类延续接口的泛型,第二个E是接口自身的泛型
public class MyArrayList <E> implements List<E>{
    //重写List方法
}

泛型的继承和通配符

泛型不具备继承性,但是数据具备继承性。可以使用泛型的通配符对数据类型进行限定,?表示不确定的类型,也可以多加条件,例如:?extends E?super E,分别表示可以传递 E 和 E 所有的子类类型、可以传递 E 和 E 所有的父类类型。

class GrandFather{}
class Father extends GrandFather{}
class Son extends Father{}

public class Main {
    public static void main(String[] args) {
        ArrayList<GrandFather> arrayList1 = new ArrayList<>();
        ArrayList<Father> arrayList2 = new ArrayList<>();
        ArrayList<Son> arrayList3 = new ArrayList<>();

        method(arrayList1);
        //以下两句话不被允许,因为泛型不具有继承性
    //    method(arrayList2);
    //    method(arrayList3);

    }
    
    //以下方法只能传递GrandFather对象
    public static void method(ArrayList<GrandFather> arrayList) {}
    
    //以下方法可以传递任意对象
    public static <E> void method1(ArrayList<E> arrayList) {}
    
    //以下方法可以传递GrandFather的任意子类对象
    public static void method2(ArrayList<? extend GrandFather> arrayList) {}
    
    //以下方法可以传递GrandFather的任意父类对象
    public static void method3(ArrayList<? super GrandFather> arrayList) {}
    
}

泛型的注意事项

class Test<T> {
    private T[] arr;
    public Test() {
        //arr = new T[10];
        //T不能直接实例化
    }
    public void init(T[] arr) { //需要通过传参来进行实例化
        this.arr = arr;
    }
}


public class REG2 {

	public static void main(String[] args) {
		int i=0;
		//CE<Integer,Double>[] carry1=new CE<Integer,Double>[5];
        //使用泛型时不能如上直接new数组,需要分两步走
		CE<Integer,Double>[] carry2=new CE[5];	//先分出数组空间
		for(i=0;i<5;i++) {
		    carry2[i]=new CE<Integer,Double>();	//再去分配每一个元素的空间
		}
		CE<Integer,Double>[] carry3;
		carry3=new CE[5];
		for(i=0;i<5;i++) {
		    carry3[i]=new CE<Integer,Double>();
		}
	}
}

最终PECS (Producer Extends Consumer Super ) 原则

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

该原则的细则可以参考Vincent-yuan的博客

集合进阶

Java的包装类

在 Java 当中,所有的基本数据类型都会对应一个包装类。

列表如下:

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

Java需要包装类的原因有以下几个:

  • 面向对象要求:Java 是一门面向对象编程语言,要求所有数据都应该是对象。但基本数据类型不是对象,它们没有成员方法和其他面向对象的特性。为了符合面向对象的编程要求,引入包装类,将基本数据类型封装成对象,使得它们有面向对象的特性。
  • 泛型要求:泛型提供了类型安全和代码重用的功能,但是泛型要求类型参数必须是对象类型,不能是基本数据类型。因此,如果想在泛型中使用基本数据类型,就必须使用对应的包装类。
  • null 值表示:包装类可以表示 null 值,而基本数据类型不能。例如在接口传参中,如果使用包装类即使前端不传参也不会报错,但是如果使用基本数据类型则会报错。

接下来,我们以 Integer 为例(Integer最为常用),来展示包装类:

Integer的基本构造

JDK5 之前的构造方法:

方法名 说明
public Integer(int value) 根据传递的整数创建对象
public Integer(String s) 根据传递的字符串创建对象
public static Integer valueOf(int i) 根据传递的整数创建对象
public static Integer valueOf(String s) 根据字符串创建对象
public static Integer valueOf(String s, int radix) 根据传递的字符串和进制创建对象
public class Main {
    public static void main(String[] args) {
        Integer i1 = Integer.valueOf(127);
        Integer i2 = Integer.valueOf(127);
        System.out.println(i1 == i2);   //true

        Integer i3 = Integer.valueOf(128);
        Integer i4 = Integer.valueOf(128);
        System.out.println(i3 == i4);   //false
        
        /*
        * Integer会先把-128到127存入数组中,如果valueOf的值在该范围内,则会复用
        * 超出找个范围再去new
        * -128~127在实际开发中用得比较多,所以这么做可以节省内存
        * */

        Integer i5 = new Integer(128);
        Integer i6 = new Integer(128);
        System.out.println(i5 == i6);   //false,因为 == 直接比较地址值,new出来的两个对象地址值不一样,所以false
    }
}
public class Main {
    public static void main(String[] args) {
        //以前的Integer计算
        Integer integer1 = Integer.valueOf(1);
        Integer integer2 = Integer.valueOf(2);
        //先利用intValue拆箱,把数据变回基本数据类型
        int result = integer1.intValue() + integer2.intValue();
        //再利用new装箱,把数据变回包装类
        Integer integer3 = new Integer(result);
        System.out.println(integer3);   //3


        //在JDK5的时候提出了一个机制:自动装箱和自动拆箱
        //自动装箱:自动把基本数据类型转换为包装类
        //自动拆箱:自动把包装类转换为基本数据类型
        Integer integer4 = 10;  //自动装箱,底层还是调用valueOf,不过不需要我们自己写了
        Integer integer5 = 20;  //在JDK5之后,int和Integer的构造方式相同
        Integer integer6 = integer4 + integer5; //可以直接使用 + 运算符
        System.out.println(integer6);   //30
        
    }
}

Integer常见方法

方法名 说明
public static String toBinaryString(int i) 得到二进制
public static String toOctalString(int i) 得到八进制
public static String toHexString(int i) 得到十六进制
public static int parseInt(String s) 将字符串类型的整数转成 int 类型整数
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        //把整数转成二进制、八进制、十六进制
        String binaryString = Integer.toBinaryString(100);
        System.out.println(binaryString);   //1100100
        String octalString = Integer.toOctalString(100);
        System.out.println(octalString);    //144
        String hexString = Integer.toHexString(100);
        System.out.println(hexString);  //64

        //String转为int
        int index = Integer.parseInt("123");
        System.out.println(index);  //123
        //parseInt当中的参数必须只包含数字,不能包含其他字符
        //8种包装类当中,除了Character都有对应的parseXxx的方法进行类型转换
        String str = "true";
        boolean flag = Boolean.parseBoolean(str);
        System.out.println(flag);   //true
        
        //int转String
        Scanner scanner = new Scanner(System.in);
        int index = scanner.nextInt();
        String str = String.valueOf(index);
        System.out.println(str);
        
    }
}

集合的体系结构

有一个总接口 Collection(单列集合),而后 Collection 又有两个子接口分别为 List 和 Set。List 接口下有三个实现类,分别是 ArrayList、LinkedList 和 Vector(Vector 已被淘汰);而 Set 接口下有两个实现类,分别是 HashSet 和 TreeSet,而HashSet 又有一个子类,叫 LinkedHashSet

flowchart TD
    id1(Collection)
    id2(List)
    id3(Set)
    id4([ArrayList])
    id5([LinkedList])
    id6([Vector])
    id7([HashSet])
    id8([TreeSet])
    id9([LinkedHashSet])
    
    id1 --> id2 & id3
    id2 --> id4 & id5 & id6
    id3 --> id7 & id8
    id7 --> id9

List 系列集合:添加的元素是有序(指顺序和插入顺序一致)、可重复、有索引的。

Set系列集合:添加的元素是无序(指顺序和插入顺序可能不一致)、不重复、无索引的。

Collection 接口

Collection 是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的。

方法名 说明
add 添加
clear 清空
remove 删除
contains 判定是否包含,底层实现是基本查找
isEmpty 判空
size 返回集合长度
import java.util.ArrayList;
import java.util.Collection;

public class Main {
    public static void main(String[] args) {
        //Collection是一个接口,不能直接创建对象,只能创建其实现类的对象
        Collection<String> collection = new ArrayList<>();

        //添加数据
        collection.add("张三");
        collection.add("李四");
        collection.add("王五");

        //判空
        System.out.println(collection.isEmpty());   //false

        //返回长度
        System.out.println(collection.size());  //3

        //判定是否包含
        System.out.println(collection.contains("张三"));  //true
        System.out.println(collection.contains("赵六"));  //false

        //删除,Collection定义的是共性的方法,所以这个时候没办法通过索引删除
        collection.remove("张三");
        System.out.println(collection); //[李四, 王五]

        //清空
        collection.clear();
        System.out.println(collection); //[]

    }
}

这里需要注意的是,通过阅读源码,我们知道,contains 方法底层是依赖 equals 方法来判定是否相同的,所以,如果集合中存储的是自定义对象的话,那么我们需要重写 equals 方法。

public class Main {
    public static void main(String[] args) {
        Collection<Student> collection = new ArrayList<>();

        Student s1 = new Student(18);
        Student s2 = new Student(19);

        collection.add(s1);
        collection.add(s2);

        Student s3 = new Student(18);

        System.out.println(collection.contains(s3));    //输出结果为false,原因是s3与s1不同址

    }
}

迭代器

专门用于遍历容器,最大的特点是不依赖索引

方法名称 说明
boolean hasNext() 判断当前位置是否有元素,有则返回true,没有则返回false
E next() 获取当前位置的元素,并将迭代器对象移动至下一个位置
void remove() 删除迭代器指向的元素
import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();

        for(int i = 0; i < 10; ++i) {
            arrayList.add(i);
        }

        //迭代器遍历
        Iterator<Integer> iterator = arrayList.iterator();
        while(iterator.hasNext()) { //用于判断当前位置是否有元素
            Integer integer = iterator.next();  //如果有,利用next获取,并将迭代器往下移动一位
            System.out.println(integer);
        }
    }
}

需要注意的是:

  • 如果迭代器已经跑到集合的最末尾(最后一个元素的下一个位置)了,还去调用 next 的话,就会抛出 NoSuchElementException 的异常。
  • 迭代器遍历完毕后,是不会复位的。
  • 循环中只能用一次 next 方法。
  • 生成迭代器之后,不能用集合的方法进行增加或删除,否则会抛出 ConcurrentModificationException 的异常,除非在增加或者删除之后,使用新的迭代器。

ArrayList 的 iterator 方法底层源码分析:

public Iterator<E> iterator() {
    return new Itr();	//该方法返回了一个Itr对象,其中,Itr是ArrayList当中的一个内部类
}

//在Itr中,包括了三个变量
int cursor;       //指向现在操作的位置
int lastRet = -1; //指向刚刚操作的位置
int expectedModCount = modCount;	//与并发修改异常有关
//modCount记录的是集合变化的次数,调用add或者remove使得集合变化时,都会引起modCount的改变


public boolean hasNext() {	//直接判断现在操作的位置是否到达底部
    return cursor != size;
}


final void checkForComodification() {
    //迭代器生成的时候,会记录本迭代器生成之后的ArrayList修改次数为expectedModCount
    //如果我们在生成迭代器之后对ArrayList进行修改,则会导致ArrayList的modCount发生改变
    //这个时候就会触发本方法抛出异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}


public E next() {
    checkForComodification();	//校验modCount次数
    int i = cursor;	//获取当前位置
    if (i >= size)	//如果超过底部,抛出异常
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;	//操作的位置往后延一位
    return (E) elementData[lastRet = i];	//更新lastRet的值,并返回lastRet对应的值
}

增强 for 和 forEach 遍历

增强 for 本身利用的就是迭代器遍历,要注意的是,所有的单列集合数组才能用增强 for 遍历。

forEach 遍历可以利用 Lambda 表达式简化书写。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {

        ArrayList<String> arrayList = new ArrayList<>();

        arrayList.add("张三");
        arrayList.add("李四");
        arrayList.add("王五");

        //增强for遍历
        for(String str : arrayList) {
            System.out.println(str);
        }

        //利用匿名内部类的形式遍历
        arrayList.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });

        //Lambda表达式简化书写
        arrayList.forEach(s->System.out.println(s));

    }
}

List 系列

List 集合:有序,有索引,可重复。

继承了 Collection 的方法,并在此基础上多了很多索引的方法:

方法名 说明
void add(int index, E element) 指定位置插入元素
E remove(int index) 删除指定位置元素
E set(int index, E element) 修改指定位置元素
E get(int index) 返回指定位置元素

此外,List 还维护了一个专属的迭代器 ListIterator,支持循环中的 add 和 remove 操作,该操作会在表中添加或者删除数据,如果调用 add 方法依旧保证迭代器的相对位置,如果调用 remove 方法,会使迭代器的 lastRet = -1。并且,还封装了 hasPrevious 和 previous 方法,用来向前遍历。但是因为其设计上需要先向后遍历再向前遍历,操作不方便,所以很少使用。

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import java.util.ListIterator;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("张三");
        list.add("李四");
        list.add("王五");
        list.add("赵六");

        //按照索引返回
        String result1 = list.get(1);
        System.out.println(result1);    //李四

        //按照索引增加
        list.add(1, "钱七");
        System.out.println(list);   //[张三, 钱七, 李四, 王五, 赵六]

        //按照索引删除
        list.remove(3);
        System.out.println(list);   //[张三, 钱七, 李四, 赵六]

        //按照索引修改
        list.set(2, "二狗");
        System.out.println(list);   //[张三, 钱七, 二狗, 赵六]
        System.out.println(list);   //[张三, 钱七, 二狗, 赵六]

        //迭代器遍历
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()) {
            String str = iterator.next();
            System.out.println(str);
        }

        //增强for
        for(String str : list) {
            System.out.println(str);
        }

        //forEach
        list.forEach(str->System.out.println(str));

        //普通for(因为List有索引,所以可以用普通for)
        for(int i = 0; i < list.size(); ++i) {
            System.out.println(list.get(i));
        }

        //列表迭代器遍历
        ListIterator<String> listIterator = list.listIterator();
        while(listIterator.hasNext()) {
            listIterator.add("测试"); //可以在遍历时插入
            String str = listIterator.next();
            System.out.println(str);
            listIterator.remove();  //也可以在遍历时删除
        }
    }
}

关于 list 的 remove 方法,有一点需要注意:即当我们在调用方法的时候,如果方法出现了重载,则会先调用与参数更加符合的方法。

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(0);

      //Integer integer = 0;      //如果需要删除Object 0,需要手动装箱
        
        list.remove(0); //当Object和index冲突的时候,优先使用remove的index重载,因为Object方法需要先装箱
        System.out.println(list);	//[0]
    }
}

ArrayList

基本用法示例
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        System.out.println(list);
        //插入
        for(int i = 0; i < 5; ++i) {
            list.add(i);
        }
        System.out.println(list);   //0 1 2 3 4

        //删除
        list.remove(2);
        System.out.println(list);   //0 1 3 4

        //获取
        Integer integer = list.get(3);
        System.out.println(integer);    //4

        //修改
        list.set(3, 5);
        System.out.println(list.get(3));    //5

        //指定位置插入
        list.add(1, 9);
        System.out.println(list);   //0 9 1 3 5
    }
}
ArrayList底层原理
  1. 利用空参创建的集合,在底层创建一个默认长度为0的数组。
  2. 添加第一个元素时,底层会创建一个新的长度为10的数组。
  3. 存满时,会扩容1.5倍
  4. 如果一次添加多个元素(例如调用addAll方法),1.5倍还放不下,则新创建数组的长度以实际为准

底层源码分析:

private static final int DEFAULT_CAPACITY = 10;	
public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;

//空参构造,创建一个长度为0的数组
public ArrayList() {	
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}


public boolean add(E e) {	//ArrayList提供给用户的接口,底层还调用了一次add
    modCount++;
    add(e, elementData, size);	//参数列表对应着:(添加的元素,底层的数组,添加元素的位置)
    return true;
}


private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)	
        elementData = grow();	//如果容器满了,则调用grow方法,扩容
    //如果容器还有空间,直接添加,并让容器大小加1
    elementData[s] = e;	
    size = s + 1;		
}


private Object[] grow() {	//grow又调用了一次重载的带参grow
    return grow(size + 1);
}


private Object[] grow(int minCapacity) {
    
    int oldCapacity = elementData.length;	//记录原来的老容量
    
    //如果容器已经有数据,则调用newLength扩容
    //oldCapacity:老容量	minCapacity - oldCapacity:理论上至少新增的大小	>>1 除以二
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        //调用copyOf方法,以新长度复制已有的数组
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } 
    //如果容器暂时没有数据,是最原始的状态,则在10和传进来的大小之间取最大值
    //注意到这里调用了max,意味着,如果数据量较小,则创建长度为10的数组
    //如果数据量很大,数组长度则按照实际的大小来
    else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}


public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    //比较至少增加的容量和默认增加的容量谁更大,意味着如果数据量很大,数组长度则按实际大小来
    int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
    //如果最终扩容的长度合法,则直接返回该长度
    if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
        return prefLength;	
    } 
    //如果最终长度溢出,调用hugeLength方法处理大长度的情况
    else {
        // put code cold in a separate method
        return hugeLength(oldLength, minGrowth);
    }
}


private static int hugeLength(int oldLength, int minGrowth) {
    int minLength = oldLength + minGrowth;
    //如果数据量实在过大,超过了int范围,则抛出异常
    if (minLength < 0) { // overflow
        throw new OutOfMemoryError(
            "Required array length " + oldLength + " + " + minGrowth + " is too large");
    } 
    //如果长度扩容到刚刚满足要求时,保留在SOFT_MAX_ARRAY_LENGTH的长度内,则返回SFOT_MAX_ARRAY_LENGTH
    else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
        return SOFT_MAX_ARRAY_LENGTH;
    } 
    //如果超过SOFT_MAX_ARRAY_LENGTH的范围,但没有超过int的范围,则返回这个新的长度
    else {
        return minLength;
    }
}

LinkedList

底层结构是双链表,查询慢,增删快,但如果操作的是首尾元素,速度也是很快的。LinkedList 本身提供了很多操作首尾元素的API。

特有方法 说明
public void addFirst(E e) 在列表开头插入指定元素
public void addLast(E e) 将指定元素追加到末尾
public E getFirst() 返回第一个元素
public E getLast() 返回最后一个元素
public E removeFirst() 删除第一个元素
public E removeLast() 删除末尾元素

值得注意的是,因为 LinkedList 是 Link 这个接口的实现类,故也可以通过索引来访问元素。

底层源码分析:

private static class Node<E> {	//内部类,节点
    E item;	//实际数据
    Node<E> next;	//前一节点
    Node<E> prev;	//后一节点

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}


transient int size = 0;	//链表长度
transient Node<E> first;	//头结点
transient Node<E> last;		//尾节点


public boolean add(E e) {	//add方法底层调用了linkLast
    linkLast(e);
    return true;
}


void linkLast(E e) {
    final Node<E> l = last;	//用l暂时维护尾节点
    final Node<E> newNode = new Node<>(l, e, null);	//新增节点的前一个节点和尾节点连接
    last = newNode;	//尾节点更新
    if (l == null)	//如果添加的节点是第一个节点,则让first也维护该节点
        first = newNode;
    else	//否则将l的下一个节点维护新增节点
        l.next = newNode;
    size++;
    modCount++;
}

Set系列

添加的元素是无序、不重复(去重)、无索引(没有带索引方法,不能用 for 循环遍历)。

Set 系列实现类:

  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
  • TreeSet:可排序、不重复、无索引

Set 接口中的方法基本上与 Collection 的 API 一致。

Set 集合基本用法

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Main {
    public static void main(String[] args) {
        //创建set集合对象
        Set<String> s = new HashSet<>();

        //添加元素,元素是唯一的
        boolean r1 = s.add("张三");
        boolean r2 = s.add("张三");
        System.out.println(r1); //true
        System.out.println(r2); //false
        System.out.println(s);  //[张三]

        //排列呈无序
        s.add("李四");
        s.add("王五");
        System.out.println(s);  //[李四, 张三, 王五]

        //利用迭代器遍历
        Iterator<String> it = s.iterator();
        while(it.hasNext()) {
            String str = it.next();
            System.out.println(str);
        }

        //利用增强for遍历
        for(String str : s) {
            System.out.println(str);
        }

        //利用lambda表达式遍历
        s.forEach(str -> System.out.println(str));
        
        
        //利用list取出s当中的值
        ArrayList<String> arrayList = new ArrayList<>(s);
        for(String str : arrayList) {
            System.out.println(str);
        }
        
    }
}

HashSet

底层原理:底层采取 Hash 表存储数据。

哈希表组成:JDK8之前采用数组 + 链表的形式;JDK8之后采用数组 + 链表 + 红黑树的形式。

数据对应的下标:int index = (数组长度 - 1) & 哈希值;其中,哈希值是利用 hashCode 方法计算出来。

哈希值特点:

  • 如果没有重写 hashCode,不同对象计算出的哈希值是不同的。(默认使用地址值计算)
  • 如果重写了 hashCode,不同对象只要属性值相同,计算出来的哈希值就是一样的。
  • 小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也可能一样,称为哈希碰撞。
public class Main {
    public static void main(String[] args) {
        Student s1 = new Student("zhangsan", 23);
        Student s2 = new Student("zhangsan", 23);

        //没有重写hashCode方法,使用地址值计算,哈希值不一样
        System.out.println(s1.hashCode());  //284720968
        System.out.println(s2.hashCode());  //189568618

        //重写hashCode方法,属性值一样,哈希值一样
        System.out.println(s1.hashCode());  //-1461067292
        System.out.println(s2.hashCode());  //-1461067292

        //哈希碰撞
        System.out.println("abc".hashCode());   //96354
        System.out.println("acD".hashCode());   //96354
    }
}

HashSet 的底层原理:

  1. 创建一个默认长度16,默认加载因子为0.75的数组,数组名为 table。
  2. 根据元素的哈希值跟数组的长度计算出应存入的位置。int index = (数组长度 - 1) & 哈希值;
  3. 判断当前位置是否为 null,如果是 null 则直接存入。
  4. 如果位置不为 null,发生了哈希碰撞,则调用 equals 方法比较属性值。如果属性值一样,不存入;如果属性值不一样,存入数组,形成链表。(JDK8之前:新元素存入数组,老元素挂在新元素下面;JDK8以后包括JDK8,新元素直接挂在老元素下面)
  5. 当数组中存入的元素数量大于长度 * 加载因子时,扩容2倍的长度。
  6. JDK8以后,当链表长度大于8并且数组长度大于等于64的时候,链表转成红黑树。(究极融合怪了属于是)

如果集合中存储的是自定义的对象,必须要重写 hashCode 和 equals 方法

LinkedHashSet

LinkedHashSet 保证了数据存入取出的有序性。(怎么存就怎么输出)

LinkedHashSet 底层数据结构依然是哈希表,只是每个元素又额外增加了一个双链表机制记录存储顺序。

意思就是拿一个双链表,跟穿针引线一样把元素按存入顺序穿起来,访问的时候就按着这个链表顺序访问就好。(究极逆天融合怪!)

往后数据如果要去重,默认使用 HashSet。如果还要加上有序,才去使用 LinkedHashSet。

TreeSet

TreeSet 是可排序的数据结构。(将数据按升序排列)底层是基于红黑树实现的。

基本方法

import java.util.Iterator;
import java.util.TreeSet;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        //创建TreeSet对象
        TreeSet<Integer> ts = new TreeSet<>();

        //添加元素
        ts.add(1);
        ts.add(3);
        ts.add(2);
        ts.add(5);
        ts.add(4);

        System.out.println(ts); //1 2 3 4 5

        //迭代器遍历
        Iterator<Integer> it = ts.iterator();
        while(it.hasNext()) {
            Integer i = it.next();
            System.out.println(i);
        }

        //增强for
        for(int i : ts) {
            System.out.println(i);
        }

        //lambda表达式
        ts.forEach(integer -> System.out.println(integer));

        
    }
}

对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。

对于自定义数据类型:

  1. 默认排序 / 自然排序:javabean 类实现 Comparable 接口指定比较规则。
  2. 比较器排序:创建 TreeSet 对象的时候,传递比较器 Comparator 指定规则。

使用原则:默认使用第一种排序方式,当第一种方式不能满足要求的时候,就使用第二种排序方式。(一般情况下,如果我们要排序包装类的时候,假设需要特殊排序,但我们又不能去修改包装类源代码,这个时候就需要第二种排序方式)

方式一:实现 Comparable 接口,需要自定义数据类型 implements Comparable 并重写虚方法。

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    //...此处javabean标准类所需代码省略
    
    @Override
    public int compareTo(Student o) {
        //指定排序规则
        //只看年龄,按照年龄升序进行排列
        return this.age - o.getAge();
    }
}

返回值如果是负值:认为当前要添加的元素是小的,存左边;若是正值,则认为是大的,存右边;若是0,则表示元素已经存在,舍弃。

方式二:使用比较器排序。

import java.util.Comparator;
import java.util.TreeSet;

public class Main {
    public static void main(String[] args) {
        //存入"c","ab","df","qwer"
        //按照长度排序,如果一样长则按照首字母排序

        //使用比较器创建TreeSet对象
        TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                int diff = o1.length() - o2.length();
                //如果长度一致,则使用默认排序
                //如果不一致,按照长度进行排序
                return (diff == 0 ? o1.compareTo(o2) : diff);
            }
        });
        
         //lambda表达式
//        TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
//                int diff = o1.length() - o2.length();
//                //如果长度一致,则使用默认排序
//                //如果不一致,按照长度进行排序
//                return (diff == 0 ? o1.compareTo(o2) : diff);
//            }
//        );

        ts.add("c");
        ts.add("ab");
        ts.add("df");
        ts.add("qwer");
        System.out.println(ts);

    }
}

复杂排序示例代码:

//...
@Override
    public int compareTo(Student o) {
        int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
        int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
        int diff = sum1 - sum2;
        //总分一样按照语文成绩排
        diff = diff == 0 ? this.getChinese() - o.getChinese() : diff;
        //语文一样按照数学成绩排
        diff = diff == 0 ? this.getMath() - o.getMath() : diff;
        //数学成绩一样按照英语成绩排
        diff = diff == 0 ? this.getEnglish() - o.getEnglish() : diff;
        //英语成绩一样按照年龄排
        diff = diff == 0 ? this.getAge() - o.getAge() : diff;
        //如果年龄一样,按照姓名的字母排序
        diff = diff == 0 ? this.getName().compareTo(o.getName()) : diff;
        //如果都一样,则认为是同一个学生,不存
        return diff;
    }
//...

如果两种方法同时存在,则以方式二排序为准。

Map 系列

map 系列属双列集合,所谓双列,就是一个键值对应一个元素值。称为键值对(Entry)。

Map 是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的。

方法名称 说明
V put(K key, V value) 添加 / 覆盖元素,返回被覆盖的元素值
V remove(Object key) 根据键删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数

基本方法:

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        //创建Map集合对象
        Map<String, String> m = new HashMap<>();

        //添加元素
        m.put("郭靖", "黄蓉");
        m.put("韦小宝", "沐剑屏");
        m.put("尹志平", "小龙女");

        //打印集合
        System.out.println(m);
        //{韦小宝=沐剑屏, 尹志平=小龙女, 郭靖=黄蓉}

        //打印长度
        System.out.println(m.size());   //3

        //按key删除
        m.remove("郭靖");
        System.out.println(m);
        //{韦小宝=沐剑屏, 尹志平=小龙女}

        //判断是否包含
        boolean keyResult = m.containsKey("郭靖");
        System.out.println(keyResult);  //false
        boolean keyValue = m.containsValue("大龙女");
        System.out.println(keyValue);   //false

        //清空
        m.clear();
        System.out.println(m);  //{}

        boolean emptyResult = m.isEmpty();
        System.out.println(emptyResult);    //true
    }
}

Map 的三种遍历方式:

  1. 键找值
  2. 键值对
  3. lambda 表达式
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

public class Main {
    public static void main(String[] args) {
        //创建Map集合对象
        Map<String, String> m = new HashMap<>();

        //添加元素
        m.put("郭靖", "黄蓉");
        m.put("韦小宝", "沐剑屏");
        m.put("尹志平", "小龙女");
        
        //键找值
        //获取所有的键,放入单列集合中
        Set<String> keys = m.keySet();
        //遍历单列集合,得到每一个键
        for(String key : keys) {
            System.out.print(key);
            //利用key得到value
            System.out.println(" = " + m.get(key));
        }

        //获取键值对对象
        //可以通过一个方法获取所有键值对
        Set<Map.Entry<String, String>> entries = m.entrySet();
        //增强for循环遍历
        for(Map.Entry<String, String> entry : entries) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key + " = " + value);
        }

        //lambda表达式遍历
        m.forEach((String s, String s2) ->  System.out.println(s + " = " + s2));

    }
}

HashMap

HashMap 是 Map 当中的一个实现类。特点是由键决定的:无序、不重复、无索引。HashMap 跟 HashSet 底层原理一样,也是哈希表结构。

底层会创建一个 entry 对象,利用 key 计算哈希值。如果发生哈希冲突,如果 key 一致,则覆盖掉原来的值。其他情况与 HashSet 一致,详解可看 HashSet。

故如果 HashMap 存储的是自定义对象,需要重写 hashCode 和 equals 方法。

注意:Set 系列的底层是new Map对象,其中,Set 的值存放于 entry 的 key 中,而 entry 的值是 PRESENT。而 PRESENT 的代码语句是:private static final Object PRESENT = new Object();这是一个无法被修改的静态对象,所有键值共用。

底层源码分析:

static class Node<K,V> implements Map.Entry<K,V> {	//节点内部类实现了Entry接口
    final int hash;	//哈希值
    final K key;	//键
    V value;		//值
    Node<K,V> next;	//产生hash冲突时用来挂新元素的
    //...
}

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {	//红黑树的节点
    TreeNode<K,V> parent;  //双亲节点
    TreeNode<K,V> left;		//左结点
    TreeNode<K,V> right;	//右节点
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;	//是否是红色节点
    //...
}

transient Node<K,V>[] table;	//HashMap底层的数组
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //数组默认的长度为16
static final float DEFAULT_LOAD_FACTOR = 0.75f;	//默认的加载因子为0.75
//当数组内部元素超过容量*加载因子时,数组会扩容为原来的两倍
static final int MAXIMUM_CAPACITY = 1 << 30;	//HashMap的最大长度为1 << 30


public HashMap() {	//默认构造,这个时候只是确定加载因子,还没有构造数组
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}


public V put(K key, V value) {	//当调用put方法时,底层会去调用putVal方法
    //以下代码中,第一个false表示是否覆盖key所对应的元素
    return putVal(hash(key), key, value, false, true);	
}


//添加元素至少要考虑三种情况:
//1. 数组位置为null
//2. 数组位置不为null,键重复,元素覆盖
//3. 数组位置不为null,键不重复,挂在下面形成链表或红黑树
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    
    Node<K,V>[] tab; //定义一个局部变量,开在栈中,存取速度更快
    Node<K,V> p; //临时第三方变量,用来记录键值对
    int n;	//当前数组的长度
    int i;	//表示索引
    
 	tab = table;	//将哈希表中的数组赋值给栈中的数组
    n = tab.length;	//将数组的长度赋值给n
    if (tab == null || n == 0) {
        //如果当前是第一次添加数据,底层会创建一个默认加载因子为0.75,长度为16的数组
        //如果不是第一次添加数据,会看数组中的元素是否到达了扩容条件
        //如果达到了扩容条件,底层会把数组扩容2倍,并把老数据全部转移到新的表中
        tab = resize();
        n = tab.length;
    }
     
    i = (n - 1) & hash;	//索引值为 = (数组长度 - 1) & 哈希值
    p = tab[i];	//获取数组中对应索引的数据
    
    if (p == null) {	//如果这个位置没有数据,则直接添加数据
        tab[i] = newNode(hash, key, value, null);
    }
    else {
        Node<K,V> e; 
        K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))) {
            //如果产生了hash冲突,并且key值也相等,这个时候记录当前要被覆盖的节点
            //(k = p.key) == key || (key != null && key.equals(k))
            //以上代码目的是利用 == 和 equals 进行二重判等,防止出现利用地址值判等的情况
            e = p;
        }
        //不需要覆盖,但是需要挂在红黑树上
        else if (p instanceof TreeNode) {
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        }
        //不需要覆盖,但是需要挂在链表上
        else {
            for (int binCount = 0; ; ++binCount) {	//遍历链表
                if ((e = p.next) == null) { 
                    //遍历到链表的末尾,将节点挂在链表上
                    p.next = newNode(hash, key, value, null);
                    //如果这个时候链表的长度过长,则将链表转化为红黑树
                    //这里的TREEIFY_THRESHOLD = 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                //如果在链表当中碰到了hash冲突并且key值相等的情况,同样记录需要被覆盖的节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break;
                }
                //每一次循环都记录一下被遍历到的节点
                p = e;
            }
        }
        //如果节点需要覆盖,则覆盖节点并返回旧节点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;	//覆盖了值
		   }
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    ++modCount;	//更新容器的修改次数
    if (++size > threshold) {	//threshold记录的是数组长度 * 加载因子
        //如果元素的大小满足扩容条件,则调用resize方法,对容器进行扩容
	    resize();
    }
    afterNodeInsertion(evict);
    return null;	//表示当前没有覆盖任何元素,返回null
}

LinkedHashMap

由键决定:有序(存储和取出的顺序一致)、不重复、无索引。

在哈希表上多了双链表机制存储存放的顺序。具体细节同 LinkedHashSet。

TreeMap

底层同 TreeSet,底层使用红黑树。由键决定:不重复、无索引、可排序(对键进行排序)。

两种排序规则:

  1. 实现 Comparable 接口,指定比较规则。
  2. 创建集合时传递 Comparator 比较器对象,指定比较规则。

其余具体细节同 TreeSet。

底层原码分析:

private final Comparator<? super K> comparator;	//比较器
private transient Entry<K,V> root;	//根节点
private transient int size = 0;	//节点个数
private static final boolean RED   = false;
private static final boolean BLACK = true;


static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;	//键
    V value;	//值
    Entry<K,V> left;	//左节点
    Entry<K,V> right;	//右结点
    Entry<K,V> parent;	//双亲节点
    boolean color = BLACK;	//节点颜色
}


//空参构造,没有比较器
public TreeMap() {
    comparator = null;
}


//带参构造,传入比较器
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}


//put方法,底层调用了三个参数的put方法
public V put(K key, V value) {
    return put(key, value, true);//参数列表:(键,值,当键重复的时候,是否需要覆盖值)
}


private V put(K key, V value, boolean replaceOld) {
    Entry<K,V> t = root;	//获取根节点的地址值,将其赋值给局部变量
    //第一次添加,直接添加元素进map中
    if (t == null) {
        addEntryToEmptyMap(key, value);
        return null;	//return null,表示没有覆盖任何元素
    }
    
    int cmp;	//表示两个元素的键比较之后的结果
    Entry<K,V> parent;	//要添加元素的双亲节点
    Comparator<? super K> cpr = comparator;	//当前的比较规则,默认为null
    //如果传递了比较器对象,则以传递的比较器规则为准
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                }
                return oldValue;
            }
        } while (t != null);
    } 
    //如果没有传递比较器对象,则按照默认排序规则为准
    else {
        //将键强转为Comparable类型,所以我们需要key实现Comparable接口
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            //把根节点赋值给双亲节点
            parent = t;
            //调用key重写的compareTo方法
            cmp = k.compareTo(t.key);
            //较小值往左插
            if (cmp < 0)
                t = t.left;
            //较大值往右插
            else if (cmp > 0)
                t = t.right;
            //一样大会覆盖
            else {
                V oldValue = t.value;
                if (replaceOld || oldValue == null) {
                    t.value = value;
                }
                return oldValue;
            }
        } while (t != null);
    }
    //添加节点,该方法会按照红黑树的规则对树的结构进行修改
    addEntry(key, value, parent, cmp < 0);
    return null;
}

注意:

  • 使用 TreeMap 的时候是不需要重写 hashCode 和 equals 方法的。
  • HashMap 底层的红黑树是默认使用 hash 值来进行排序的,所以不需要传递比较器对象。

不可变集合

不可变集合:不可以被修改的集合。(长度不能改,内容也不能改)

  1. 如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的时间。
  2. 当集合对象被不可信的库调用时,不可变形式是安全的。

简单来讲,不想让别人修改集合中的内容时,可以使用不可变集合。只允许他人查看,不允许别人修改。

在 List、Set、Map 接口中,都存在静态的 of 方法,可以获取一个不可变的集合。

方法名称 说明
static <E> List<E> of(E...elements) 创建一个具有指定元素的 List 对象
static <E> Set <E> of(E...elements) 创建一个具有指定元素的 Set 集合对象
static <K, V> Map<K, V> of(E...elements) 创建一个具有指定元素的 Map 集合对象
import java.util.List;

public class Main {
    public static void main(String[] args) {
        //利用of关键字创建不可变集合
        List<String> list = List.of("张三", "李四", "王五", "赵六");

        //创建完毕之后,只能进行查询操作
        System.out.println(list.get(0));
        
        //错误的,该集合已经不可被修改
        //list.set(0, "钱七");
        //list.add("test");
        
        
        //当我们要获取一个不可变的Set集合时,需要保证集合当中元素的唯一性
        Set<String> set = Set.of("张三", "李四", "王五", "赵六");
        //以下代码是错误的,原因是出现了重复的值
        //Set<String> set = Set.of("张三", "李四", "王五", "赵六", "赵六");
        
        
        //同样的,不可变的Map集合也需要保证键的唯一性
        Map<String, String> map = Map.of("张三", "南京", "李四", "北京");
        //以下代码是错误的,原因是出现了重复的键
        //Map<String, String> map = Map.of("张三", "南京", "李四", "北京", "张三", "东京");
        
        
        //Map的不可变集合最多传递10个键值对,因为java官方在这里暴力重载了10个方法。。。
        //没错,纯暴力重载,原因是可变参数的语法不允许出现两个可变参数共存的情况
        //如果实在需要传如多个键值对,可以利用ofEntries方法:
        HashMap<String, String> hm = new HashMap<>();
        //先利用entrySet获取键值对集合
        Set<Map.Entry<String, String>> entrySet = hm.entrySet();
        //把entrySet变为一个数组
        //调用toArray方法,方法要求传数组,所以传一个0长度的进去
        //传0长度是因为如果传进去的数组长度 < 实际需求,那么会根据实际长度重新创建数组
        Map.Entry[] array = entrySet.toArray(new Map.Entry[0]);
        Map<String, String> map = Map.ofEntries(array);
        
        
        //JDK10之后,提供了copyOf方法,底层会将非不可变的map转化为不可变map
        Map<String, String> map1 = Map.copyOf(hm);
    }
}

集合的特殊用法

利用contains方法取集合交集:

import java.util.*;

public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();
        ArrayList<Integer> list3 = new ArrayList<>();
        Collections.addAll(list1, 1, 2, 3, 4, 5);
        Collections.addAll(list2, 2, 3, 4, 5);
        Collections.addAll(list3, 1, 4, 5);

        //取三个集合的交集
        List<Integer> list = list1.stream()
                .filter(list2::contains)
                .filter(list3::contains)
                .toList();
        System.out.println(list); //[4,5]
    }

}

利用merge方法对两个 Map 进行键值合并:

import java.util.*;
import java.util.function.BiConsumer;

public class Main {

    public static void main(String[] args) {
        HashMap<String, Integer> map1 = new HashMap<>();
        map1.put("1", 11);
        map1.put("2", 12);
        map1.put("5", 13);
        map1.put("6", 14);

        HashMap<String, Integer> map2 = new HashMap<>();
        map2.put("1", 22);
        map2.put("2", 23);
        map2.put("3", 24);
        map2.put("4", 25);

        //朴素做法
        //思路
        //遍历map1,把map1的内容加到map2上去
//        for (String index : map1.keySet()) {
//            int value = map2.getOrDefault(index, 0);
//            map2.put(index, value + map1.get(index));
//        }

        //使用merge函数
        map1.forEach(new BiConsumer<String, Integer>() {
            //先利用forEach对map1进行遍历,得到key和value
            @Override
            public void accept(String s, Integer integer) {
                //map2调用merge方法自动加和
                map2.merge(s, integer, Integer::sum);
            }
        });

        //Lambda表达式优化
        map1.forEach((key, value)->map2.merge(key, value, Integer::sum));

        System.out.println(map2);
    }

}

可变参数

方法形参的个数是可以发生改变的,实际上是一个参数数组。方法的形参中最多只能写一个可变参数。如果除了可变参数以外,还有其他参数,那么可变参数需要写在最后。

格式:数据类型...数据名称

public class Main {
    public static void main(String[] args) {

        int result = sum(1, 2, 3, 4);
        System.out.println(result); //10

        int result1 = sum(1, 2, 3);
        System.out.println(result1);    //6

    }

    public static int sum(int...number) {
        int result = 0;
        for(int i : number) {   //number是一个数组,使用增强for循环遍历
            result += i;
        }
        return result;
    }
}

Collections

是集合工具类不是集合

常用API有:

方法名称 说明
public static <T> boolean addAll(Collection<T> c, T...elements) 批量添加数据
public static void shuffle(List<?> list) 打乱 List 集合元素顺序
public static <T> void sort(List<?> list) 排序
public static <T> void sort(List<?> list, Comparator<T> c) 按照指定规则排序
public static <T> int binarySearch(List<?> list, T key) 以二分查找法查找元素
public static <T> void copy(List<T> dest, List<T> src) 拷贝集合中的元素
public static <T> int fill(List<T> list, T obj) 使用指定的元素填充集合
public static <T> void max/min(Collection<T> coll) 根据默认的自然排序获取最大、小值
public static <T> void swap(List<?>list, int i, int j) 交换集合中指定位置的元素
import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        //批量添加元素
        ArrayList<String> arrayList = new ArrayList<>();
        Collections.addAll(arrayList, "abc", "bcd", "efg");	//可以利用addAll方法对集合进行初始化
        System.out.println(arrayList);  //[abc, bcd, efg]

        //shuffle打乱
        Collections.shuffle(arrayList);
        System.out.println(arrayList);  //[bcd, abc, efg]
    }
}

集合的嵌套:

import java.util.*;
import java.util.function.BiConsumer;

public class Main {
    public static void main(String[] args) {
        //集合的嵌套
        HashMap<String, ArrayList<String> > hashMap = new HashMap<>();

        //数据预处理
        ArrayList<String> guangDongProvince = new ArrayList<>();
        Collections.addAll(guangDongProvince, "汕头市", "广州市", "深圳市");
        ArrayList<String> huNanProvince = new ArrayList<>();
        Collections.addAll(huNanProvince, "长沙市", "衡阳市", "娄底市");
        ArrayList<String> jiangSuProvince = new ArrayList<>();
        Collections.addAll(jiangSuProvince, "南京市", "扬州市", "苏州市");

        //数据的添加
        hashMap.put("广东省", guangDongProvince);
        hashMap.put("湖南省", huNanProvince);
        hashMap.put("江苏省", jiangSuProvince);

        //数据的遍历,使用keySet遍历
        Set<String> keySet = hashMap.keySet();
        for(String str : keySet) {
            System.out.println(str + ":");
            System.out.println(hashMap.get(str));
        }

        System.out.println("---------------");

        //数据的遍历,使用EntrySet遍历
        Set<Map.Entry<String, ArrayList<String>>> entrySet = hashMap.entrySet();
        for(Map.Entry<String, ArrayList<String>> entry : entrySet) {
            System.out.println(entry.getKey() + ":");
            System.out.println(entry.getValue());
        }

        System.out.println("---------------");

        //数据的遍历,使用foreach遍历
        hashMap.forEach((s, strings)->{
                System.out.println(s + ":");
                System.out.println(strings);
            }
        );
    }
}

Java 基础上的内容就到此结束啦╰(°▽°)╯ヾ(≧▽≦*)o,剩下的内容请移步至《Java 基础(下)》吧!


文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录