跳转到内容
Jsonq's Blog
返回

Java 基础学习

所有笔记内容基于网络上的黑马视频课程进行记录…

Table of contents

Open Table of contents

面向对象

基础

this 关键字

跟前端的 this 差不多,就近原则。

public class Test {
  private String name;

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

  public void ignoreThis() {
    // 省略 this 也可以
    System.out.println(name);
    setName("张三");
  }
}
public class Person {
 private String name;

 public void printThis() {
   System.out.println(this);
 }
}

public class Test {
 public static void main(String[] args) {
   Person person = new Person();
   System.out.println(person); // Person@1b6d3586
   person.printThis(); // Person@1b6d3586
 }
}

构造方法

不写的话 Java 会默认生成一个无参构造方法,可以 IDEA 右键 Generate 快速生成,后边有 lombok 就不这么麻烦了。

public class Person {
  String name;
  int age;

  // 无参和有参其实就是重载
  public Person() {
    System.out.println("无参构造方法");
  }

  public Person(String name, int age) {
      this.name = name;
      this.age = age;
      System.out.println("有参构造方法");
  }
}

public class Test {
  public static void main(String[] args) {
    Person person = new Person();
    System.out.println(person.name); // null 无参构造方法
    Person person1 = new Person("张三", 18)
    System.out.println(person1.name); // 张三 有参构造方法
  }
}

权限修饰符

修饰符当前类中当前包中不同包的子类不同包的非子类
private
(default)
protected
public

protected 开发中基本用不到

// com.example.a;
public class Base {
    protected int value = 42;   // protected 成员
}

// com.example.a; 同包非子类
public class SamePackage {
    public void access(Base b) {
        System.out.println(b.value);  // ✅ 可以访问(同包)
    }
}
//  com.example.b; 不同包子类
public class Subclass extends Base {
    public void access() {
        System.out.println(value);     // ✅ 可以访问(子类继承)
    }
}
// com.example.b; 不同包非子类
public class NonSubclass {
    public void access(Base b) {
        // System.out.println(b.value); // ❌ 编译错误
    }
}

JavaBean

开发中的数据实体都用这个

static 关键字

public class Student {
  public static String schoolName;
  public int age;
}

public class Test {
  public static void main(String[] args) {
    Student.schoolName = "清华大学";

    Student student = new Student();
    System.out.println(student.schoolName); // 清华大学

    Student student1 = new Student();
    System.out.println(student1.schoolName); // 清华大学
  }
}

注意事项:

高级

继承 extends

继承是面向对象三大特征之一,封装、继承、多态。

跟前端的 TS 差不多,子类 extends 父类,子类会继承父类的属性和方法(除了私有属性和方法),在实际场景中,需要继承的父类一般都是公共属性的基类。

public class Person {
  int sex=""
}

public class Student extends Person {
  int sex="";

  public void printSex() {
    int sex="未知";
    System.out.println(sex); // 未知
    System.out.println(this.sex); // 女
    System.out.println(super.sex); // 男
  }
}
public class Father {
  public void love() {
    System.out.println("三大件:手表、自行车、缝纫机");
  }
}

public class Son extends Father {
  @Override
  public void love() {
    System.out.println("三大件:房、车、钱");
  }

  // 方法名相同,但是入参/返回值不一致,就不属于重写,属于重载
  public void love(String gender){
    System.out.println("三大件:房、车、钱");
  }
}
  1. 子类无法继承父类的构造方法
  2. 子类在初始化时,系统会自动调用父类的构造方法,父类构造方法早于子类构造方法执行,因为子类很有可能通过 super 访问父类的变量或方法
  3. 基于第 2 点,子类所有的构造方法,首行默认添加 super() 从而完成调用父类的构造方法
public class Father {
  public Father() {
    System.out.println("父类无参构造方法");
  }
}

public class Son extends Father {
  public Son() {
    //  super() // 默认添加
    System.out.println("子类无参构造方法");
  }
}

new Son(); // 打印顺序:父类无参构造方法 -> 子类无参构造方法

Object/Objects

  1. Object

跟 js 一样,万物皆对象,Object 是所有类的父类,所有的类都继承了 Object 类。

通过 System.out.println() 输出时,会自动调用 toString() 方法,一般 JavaBean 类都会重写 toString() 方法,返回对象的属性值。

== 只适合比较基本类型值,判断两个对象是否相等,使用 a.equals(b),开发中,通常推荐重写 equals() 方法来自定义比较

  1. Objects

final 关键字

final 修饰的成员变量,要么直接写值,要么在构造方法中赋值。final 修饰的变量时常量,命名规范上应该是 全大写单词之间 _ 连接。

抽象类 abstract

父类抽象类中只定义共性的行为,不定义具体的实现,abstract 修饰的方法所在的类必须是抽象类,子类要么也是抽象类,要么强制实现抽象方法

// 比如每个人都有工作,但是每个人的工作并不一定一样
public abstract class Person {
    public abstract void work();
}

class PersonA extends Person {
    @Override
    public void work() {
        System.out.println("厨师");
    }
}

class PersonB extends Person {
    @Override
    public void work() {
        System.out.println("码农");
    }
}

抽象类注意事项:

abstract 关键字冲突:

接口 interface

接口就是对行为的抽象(abstract),如果一个 class 类全都是对规则的定义(抽象类),那么这个类就可以改写成接口。

接口和抽象类一样,子类在实现(implements)接口时,必须实现接口中的所有抽象方法,要不然子类就也成为抽象类。

public abstract class Person {
  public abstract void work();
  public abstract void eat();
}

public interface Person {
  public abstract void work();
  public abstract void eat();
}

接口的特点:

public interface Person {
  String NAME = "张三";

  private void fn() {} // JDK9 开始支持,不会被子类实现

  void work(); // 等价于 public abstract void work();

  // public void eat(){} // 报错
  // default void eat() {} // JDK8 显式添加 default 修饰符可以标记成**可选实现的抽象类**,子类可以不实现,也可以实现
}

// Person person = new Person(); // 报错 因为抽象
// Person.NAME = "李四"; // 报错 因为 final 修饰
System.out.println(Person.NAME);

如果方法的形参是一个 interface 接口,想要传入参数,就需要一个实现此 interface 的实现类。结合匿名内部类看

// Person 是一个 interface
interface Person{
  void eat();
}

public static void main(String[] args){
  usePerson();
}

public static void usePerson(Person p){
  p.eat(new Coder());
}

// interface 无法 new,只能通过实现类来传入
class Coder implements Person{
  @Override
  public void eat() {
    System.out.println("码农吃东西");
  }
}

多态

有以下特点的才是多态:

虽然在转换成父类时无法使用子类的特有方法,但是可以用 instanceof 来判断属于哪个子类,进而访问子类的特有方法。

public class Person {
  public void eat() {
    System.out.println("吃吃吃");
  }
}
// 有继承 有方法重写
public class Student extends Person {
  @Override
  public void eat() {
    System.out.println("不仅吃还喝");
  }
}

Student student = new Student(); // 子类引用指向子类对象
Person person = new Student(); // 父类引用指向子类对象

多态的应用场景

abstract class Animal {
  void eat();
}
class Dog extends Animal {
  @Override
  public void eat() {
    System.out.println("狗吃肉");
  }
}
class Cat extends Animal {
  @Override
  public void eat() {
    System.out.println("猫吃鱼");
  }
}

public void useAnimal(Animal animal){
  animal.eat();
}

// Usage
useAnimal(new Dog());
useAnimal(new Cat());

内部类

// 成员内部类
class Outer{
  class Inner{}
}
Outer.Inner oi= new Outer.new Inner()

// 静态内部类
class Outer{
  static class Inner{}
}

Outer.Inner oi = new Outer.Inner();
interface Person{
  void eat();
}

public static void main(String[] args){
  // 没有 implements Person,直接 new Person(){} 没有名字,这就是一个匿名内部类
  // 将 继承(extends)/实现(implements)、方法重写、创建对象合并成一行代码
  new Person(){
    @Override
    public void eat() {
      System.out.println("匿名内部类在吃");
    }
  }.eat(); // new 之后就是一个对象了,可以调用方法,只是不直观
}

常用举例:

interface Person{
  void eat();
}

// 👍 new Person 的时候 IDEA 会自动补全 Override 的方法
// 重写的方法少了这样写是可以的,如果太多尽量抽出去
usePerson(new Person(){
  @Override
  public void eat() {
    System.out.println("匿名内部类在吃");
  }
})

public static void usePerson(Person person){
  person.eat();
}

Lamba 表达式

Lamba 表达式是 JDK8 引入,只能简化函数式接口的匿名内部类写法(匿名内部类被重写的方法形参)-> {}

满足以下条件的是函数式接口:

@FunctionalInterface
interface Person{
  void eat();
}

public static void main(String[] args){
  // 匿名内部类写法
  usePerson(new Person(){
    @Override
    public void eat() {
      System.out.println("匿名内部类在吃");
    }
  })

// eat 没有形参,因此为空
  usePerson(() -> {
    System.out.println("匿名内部类在吃");
  })
  // 由于只有一行代码,因此可以省略 {},跟 JS 的箭头函数差不多
  usePerson(() -> System.out.println("匿名内部类在吃"));
}

public static void usePerson(Person person){
  person.eat();
}

匿名内部类在警经过编译之后,会生成单独的字节码文件,而 Lamba 不会。

常用 API

String

String str = "Hello";
System.out.println(str.length()); // 5
System.out.println(str.charAt(0)); // H
System.out.println(str.toCharArray()); // [H, e, l, l, o]
System.out.println(str.equals("Hello")); // true
System.out.println(str.equalsIgnoreCase("hello")); // true
System.out.println(str.substring(0, 2)); // He
System.out.println(str.substring(2)); // llo
System.out.println(str.replace('l', 'x'));
System.out.println(str); // Hello 不会更改原始字符串
System.out.println(str.toUpperCase()); // HELLO
System.out.println(str.toLowerCase()); // hello
System.out.println(str.startsWith("H")); // true
System.out.println(str.endsWith("o")); // true
System.out.println(str.contains("el")); // true
System.out.println(str.indexOf('l')); // 2
System.out.println(str.lastIndexOf('l')); // 2
System.out.println(str.isEmpty()); //  false
System.out.println(str.hashCode()); // 69609650
System.out.println(str.concat(" World")); // Hello World
System.out.println(str.split(" / ")); // [Ljava.lang.String;@2f4d3709

ArrayList

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add(1, "B"); // 此时数组为 {'A', 'B'},index 不能大于 数组集合 size
System.out.println(arrayList.get(1)); // 输出 B
System.out.println(arrayList.size()); // 输出 2
arrayList.remove("A"); // 此时数组为 {'B'}
arrayList.remove(0); // 此时数组为 {}
arrayList.add("A1"); // 此时数组为 {'A1'}
arrayList.set(0, "C"); // 此时数组为 {'C'},set 的索引必须存在

StringBuilder/StringBuffer

StringBuilder sb = new StringBuilder();
System.out.println(sb.append("Hello"));
System.out.println(sb.append("World")); // 输出 HelloWorld
System.out.println(sb.insert(2, "Java")); // 输出 HeJavalloWorld
System.out.println(sb.delete(2, 5)); // 输出 HealloWorld 删除索引 2~5不包含5索引的字符
System.out.println(sb.reverse()); // 输出 dlroWollaeH 反转字符串

StringJoiner

在循环拼接字符串时,StringJoiner 的代码会更加简洁。

// StringBuilder 拼接字符串
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < 10; i++) {
    if (i == 9) {
        sb.append(i);
    } else {
        sb.append(i).append(",");
    }
}
sb.append("]");
System.out.println(sb);

// StringJoiner 拼接字符串
StringJoiner sj = new StringJoiner(",", "[", "]");
for (int i = 0; i < 10; i++) {
    sj.add(i + "");
}
System.out.println(sj);

Math System Runtime BigDecimal

Math 部分 API 跟 js 差不多。

API描述
Math.abs(x)返回 x 的绝对值
Math.ceil(x)返回 x 的最接近的整数,且不小于 x(向上取整)
Math.floor(x)返回 x 的最接近的整数,且不大于 x(向下取整)
Math.max(x,y)返回 x 和 y 中的最大值
Math.min(x,y)返回 x 和 y 中的最小值
Math.random()返回 0 到 1 之间的随机数不包含 1
Math.pow(x,y)返回 x 的 y 次方

Runtime 是一个单例,指 Java 所在的运行环境(系统内部使用)

API描述
Runtime.getRuntime()获取 Runtime 实例
Runtime.availableProcessors()获取当前运行环境可用的 CPU 数
Runtime.freeMemory()获取当前运行环境可用内存
Runtime.totalMemory()获取当前运行环境总内存
Runtime.maxMemory()获取当前运行环境最大内存
Runtime.gc()运行垃圾回收器
Runtime.exec(command)运行命令
Runtime.exit(status)终止当前运行的进程 非 0 状态码表示异常终止

System 指当前系统环境(一般对外使用)

API描述
System.getProperty(key)获取系统属性
System.out.println(x)输出 x
System.currentTimeMillis()获取当前系统时间戳

BigDecimal 是 Java 语言中用于解决浮点数运算精度丢失的问题的类。

// 1. 正确的初始化方式:使用 String,避免精度丢失
BigDecimal num1 = new BigDecimal("10.5");
BigDecimal num2 = new BigDecimal("3.2");

BigDecimal sum = num1.add(num2); // 10.5 + 3.2 = 13.7
BigDecimal difference = num1.subtract(num2); //  10.5 - 3.2 = 7.3
BigDecimal product = num1.multiply(num2);  // 10.5 * 3.2 = 33.60 精度(scale)变成 2 位时自动计算的结果

BigDecimal num3 = new BigDecimal("10");
BigDecimal num4 = new BigDecimal("3");

// 场景 A:❌ 10 / 3? 不行,这会报错,因为除不尽  必须指定 精度(小数位数) 和 舍入模式(RoundingMode)
//  BigDecimal divideExact = num3.divide(num4);
// 场景 B:除不尽,指定保留2位小数,四舍五入
BigDecimal divide1 = num3.divide(num4, 2, RoundingMode.HALF_UP);  // 10 / 3 = 3.33
// 场景 C:除不尽,指定保留4位小数,直接截断(不四舍五入)
BigDecimal divide2 = num3.divide(num4, 4, RoundingMode.DOWN); // 10 / 3 = 3.3333

// ==================== 常用工具方法:setScale(设置小数位数) ====================
BigDecimal money = new BigDecimal("123.4567");
// 保留2位小数,四舍五入(常用于金额)
BigDecimal roundedMoney = money.setScale(2, RoundingMode.HALF_UP); // 123.46

// ==================== 比较大小 ====================
BigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");

// 比较大小必须使用 compareTo:返回 0 相等,1 大于,-1 小于
System.out.println("a.equals(b): " + a.equals(b)); // false equals 会比较数值精度
System.out.println("a.compareTo(b): " + a.compareTo(b)); // 0 (表示相等)

JDK 8 的时间类

  1. LocalDate:日期,如 2023-01-01。
  2. LocalTime:时间,如 10:30:00。
  3. LocalDateTime:日期和时间,如 2023-01-01 10:30:00。
  4. ZoneId:时区,如 UTC、Asia/Shanghai 等。
  5. ZoneDateTime:日期和时间和时区,如 2023-01-01 10:30:00 UTC。
  6. Duration:时间间隔,如 1 小时 30 分钟。
  7. Period:日期间隔,如 1 年 2 个月 3 天。
  8. Instant:时间戳,如 2023-01-01T10:30:00Z。
  9. DateTimeFormatter:用于格式化和解析日期和时间。
// 1. LocalDate —— 只含日期(年-月-日)
LocalDate date = LocalDate.of(2023, 1, 1);          // 创建指定日期
System.out.println(date);                           // 2023-01-01
System.out.println(date.plusDays(10));              // 加10天 → 2023-01-11
System.out.println(date.minusMonths(2));            // 减2个月 → 2022-11-01

// 2. LocalTime —— 只含时间(时:分:秒)
LocalTime time = LocalTime.of(10, 30, 0);           // 创建指定时间
System.out.println(time);                           // 10:30:00
System.out.println(time.plusHours(1));              // 加1小时 → 11:30:00
System.out.println(time.minusMinutes(15));          // 减15分钟 → 10:15:00

// 3. LocalDateTime —— 日期 + 时间
LocalDateTime dateTime = LocalDateTime.of(2023, 1, 1, 10, 30); // 创建指定日期时间
System.out.println(dateTime);                       // 2023-01-01T10:30
System.out.println(dateTime.plusDays(5).plusHours(2)); // 加5天再+2小时 → 2023-01-06T12:30

// 4. ZoneId —— 时区标识
ZoneId zone1 = ZoneId.of("UTC");                    // 获取UTC时区
ZoneId zone2 = ZoneId.of("Asia/Shanghai");          // 获取上海时区
System.out.println(zone1);                          // UTC
System.out.println(zone2);                          // Asia/Shanghai

// 5. ZonedDateTime —— 带时区的日期时间
ZonedDateTime zdt = ZonedDateTime.of(2023, 1, 1, 10, 30, 0, 0, zone2);
System.out.println(zdt);                            // 2023-01-01T10:30+08:00[Asia/Shanghai]
System.out.println(zdt.withZoneSameInstant(zone1)); // 转换到UTC时区同一时刻 → 2023-01-01T02:30Z[UTC]

// 6. Duration —— 秒/纳秒级的时间间隔(适合时、分、秒)
LocalTime t1 = LocalTime.of(8, 0);
LocalTime t2 = LocalTime.of(9, 30);
Duration duration = Duration.between(t1, t2);       // 计算两个时间之间的间隔
System.out.println(duration);                       // PT1H30M(表示1小时30分)
System.out.println(duration.toMinutes() + " minutes"); // 转为总分钟数 → 90 minutes

// 7. Period —— 年/月/日的日期间隔(适合年、月、日)
LocalDate d1 = LocalDate.of(2023, 1, 1);
LocalDate d2 = LocalDate.of(2024, 3, 15);
Period period = Period.between(d1, d2);             // 计算两个日期之间的间隔(年/月/日)
System.out.println(period);                         // P1Y2M14D(1年2个月14天)
System.out.println(period.getYears() + " years, " + period.getMonths() + " months, " + period.getDays() + " days");
// → 1 years, 2 months, 14 days

// 8. Instant —— 时间戳(从1970-01-01T00:00:00Z开始的秒数)
Instant now = Instant.now();                        // 获取当前UTC时间戳
System.out.println(now);                            // 例如 2026-08-06T10:30:00.123Z
Instant epoch = Instant.ofEpochSecond(0);           // 从1970-01-01开始的0秒
System.out.println(epoch);                          // 1970-01-01T00:00:00Z

// 9. DateTimeFormatter —— 自定义日期时间的格式化和解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dt = LocalDateTime.of(2023, 1, 1, 10, 30);
String formatted = dt.format(formatter);            // 按指定格式输出
System.out.println(formatted);                      // 2023-01-01 10:30:00
LocalDateTime parsed = LocalDateTime.parse("2023-01-01 10:30:00", formatter); // 按格式解析字符串
System.out.println(parsed);                         // 2023-01-01T10:30

集合

Collection

Collection 是所有集合的父接口。

List 和 Set 的区别就是 Set 会自动去重。

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
System.out.println(list); // [a, b, b]

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("b");
System.out.println(set); // [a, b]

Collection 常用方法:

  1. add(E e):添加元素。相关的有 addAll(Collection<? extends E> c) 把目标集合中的元素添加到当前集合中。
  2. clear():清空集合。
  3. contains(Object o):判断集合中是否包含某个元素。
  4. isEmpty():判断集合是否为空。
  5. remove(Object o):删除集合中的某个元素。
  6. size():获取集合的长度。
  7. toArray():将集合转换为数组。

Collection 迭代器:

  1. iterator():获取迭代器。鸡肋,不如直接用 for 循环。
  2. for 循环,跟 js 一样。
  3. hasNext():判断是否还有下一个元素。
  4. next():获取下一个元素。
  5. forEach(Consumer<? super E> action):遍历集合。
  6. removeIf(Predicate<? super E> filter):删除符合条件的元素。
  7. removeAll(Collection<?> c):删除集合中的所有元素。
ArrayList<String> list = new ArrayList<>(List.of("Apple", "Banana", "Cherry"));

// Iterator + next // 如果需要在循环过程中 add remove 使用 list.listIterator(),不然会出现并发异常
Iterator<String> iterator = list.iterator();
System.out.println(iterator.next()); // Apple 取当前位置的数据并移位
System.out.println(iterator.next()); // Banana

// hasNext 这个示例不如直接用 for 循环
while (iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
}

// for 循环
for (String s : list) {
    System.out.print(s + " ");
}

// for 循环的 Lamba 写法 正常写法 list.forEach(item->{ System.out.println(item); });
// 最简写法:方法引用写法 跟 js 的函数简写思路差不多 onChange={setValue}
list.forEach(System.out::println);
// 完整写法:匿名内部类 这个建议看一下 forEach 源码,本质就是方法回调。
// 传入 action 然后回调 action.accept(item)
list.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
  });

数据结构

数组和链表的区别

栈/队列

链表

每个链表包含两部分,数据域和指针域。数据域存放数据,指针域存放下一个节点的指针。

每个节点的指针指向下一个节点的地址。在查找时,只能从头开始查找。

比如找元素 D,则从头开始,头存储指向 A 指针地址,找到 A 指向下一个节点 B 的指针地址,B 再找到 C 的地址,一次类推找到 D。

每个节点有两个指针,一个指向前一个节点,一个指向下一个节点。

在查找时,通过判断当前节点是距离头部还是距离尾部更近,从而选择从哪开始查找。

泛型类

JDK5 引入的泛型,跟 TS 的泛型差不多,不过由于 js 的灵活性,ts 泛型的可操作性比 Java 要更高。

常用的泛型命名:E 表示 element 元素,T 表示 type 类型,K 表示 key 键,V 表示 value 值。

泛型类

// ====== 非静态泛型 ======
class Pair<K, V> {
  private K key;
  private V value;

  public Pair(K key, V value) {
    this.key = key;
    this.value = value;
  }
}

// 创建时就确定了方法的泛型
// 如果声明了泛型但是没传入,那么 K 和 V 都会变成 Object
Pair<String, Integer> pair = new Pair<>("Apple", 1);

System.out.println(pair.getKey());
System.out.println(pair.getValue());
System.out.println(pair);

// ====== 静态泛型 ======
class Pair {
  // 静态方法泛型
  public static<T> printArray(T[] arr) {
    for (T t : arr) {
      System.out.println(t);
    }
  }
}

泛型接口

跟泛型类差不多,类实现有泛型的 interface 时,有两种操作方式:

// 方法一:常规 interface 泛型
interface Inter<T> {
  boolean compare(T a, T b);
}

class Apple implements Inter<Integer> {
  @Override
  public boolean compare(Integer a, Integer b) {
    return false;
  }
}

// 方法二:实现类 泛型
class Apple<T> implements Inter<T> {
  @Override
  public boolean compare(T a, T b) {
    return false;
  }
}

Apple<Integer> apple = new Apple<>();// 此时 compare 方法的泛型 T 被指定为 Integer
Apple<String> apple = new Apple<>(); // 此时 compare 方法的泛型 T 被指定为 String

泛型通配符

当泛型类型不知道,或者传入的类型比较多,无法确定具体类型时,可以使用通配符,? 表示任意类型,在实际使用中,如果不想让任意类型传入,可以使用 extendssuper 来约束传入的类型。

interface Employee {
 void work();
}

class Leader implements Employee {
  @Override
  public void work() {
    System.out.println("Leader is working");
  }
}
class Manager implements Employee {
  @Override
  public void work() {
    System.out.println("Manager is working");
  }
}
class Coder implements Employee {
  @Override
  public void work() {
    System.out.println("Coder is working");
  }
}

ArrayList<Coder> coders = new ArrayList<>();
coders.add(new Coder());
ArrayList<Manager> managers = new ArrayList<>();
managers.add(new Manager());
ArrayList<Leader> leaders = new ArrayList<>();
leaders.add(new Leader());

method(coders);
method(managers);
method(leaders);

// 1. 这是第一种封装泛型的方法
public static<T> void method(ArrayList<T> arrayList){
  arrayList.forEach(Employee::work);
}

// 2. 还可以使用通配符,用 ? 表示任意类型,如果不想类型太广泛,可以使用 extends 或 super 进行约束
// ? extends Employee 表示传入的类型必须是 Employee 或是继承 Employee 子类
public static void method(ArrayList<? extends Employee> arrayList){
  arrayList.forEach(Employee::work);
}

Set

存储无序,没有索引(没有提供索引相关的 API),数据不重复。

TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("c");
treeSet.add("a");
treeSet.add("b");
treeSet.add("q");
treeSet.add("a");
System.out.println(treeSet); // [a, b, c, q]

HashSet<String> hashSet = new HashSet<>();
hashSet.add("c");
hashSet.add("a");
hashSet.add("b");
hashSet.add("q");
treeSet.add("a");
System.out.println(hashSet); // [a, q, b, c] 注意和 TreeSet 的打印不一样

LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("c");
linkedHashSet.add("a");
linkedHashSet.add("b");
linkedHashSet.add("q");
linkedHashSet.add("a");
System.out.println(linkedHashSet); // [c, a, b, q] 按插入顺序排序

TreeSet 底层结构

TreeSet 底层结构是红黑树,红黑树是一种自平衡的二叉查找树,是增删改查性能相对比都较好的数据结构。

image

TreeSet 排序方式

TreeSet 默认排序规则是自然排序,也可以指定比较器排序。

TreeSet 在添加对象时,对象必须实现 Comparable 接口,并重写 compareTo 方法,否则会报错,因为对象本身不具有比较性。

重写 compareTo 方法时,return 的值:负数去左边,正数去右边,0 忽略

// × 使用 TreeSet 时报错
class Student {
//...
}

// √ 实现 Comparable 接口
class Student implements Comparable<Student> {
  // ...
  @Override
  public int compareTo(Student o) {
    return this.age - o.age; // 年龄升序
  }
}

TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(new Student("张三", 20));
treeSet.add(new Student("李四", 22));
treeSet.add(new Student("王五", 18));
treeSet.add(new Student("赵六", 22));
// [Student{name='王五', age=18}, Student{name='张三', age=20}, Student{name='李四', age=22}]
// “赵六” 的数据并没有被添加,因为 “赵六” 的年龄和 李四 的年龄相同,所以不存。
System.out.println(treeSet);

以上会出现一个问题,虽然通过 compareTo 方法指定了以 age 为排序规则防止了报错,但是也出现了一个问题:

不同的人,同样的年龄就无法添加,这是不合理的,因此需要将 name 也参与进排序规则中。

class Student implements Comparable<Student> {
  // ...
  @Override
  public int compareTo(Student o) {
    // 需求:age 作为主要排序,name次要排序,当 age 和 name 相同时,不删除数据,保留
    const ageResult = this.age - o.age;
    // 如果 age 相等,则比较 name
    const nameResult = ageResult == 0 ? this.name.compareTo(o.name): ageResult;
    // 如果 name 仍然相等,则返回 1 或者任意非 0 数据,表示不删除数据,保留
    return nameResult == 0 ? 1: nameResult;
  }
}

比如以下情况,默认情况下,TreeSet 对 Integer 类型的数据进行排序时,会使用 Integer 内部 compareTo 自然排序方式进行排序,只能是是升序(从小到大),无法让 Integer 再添加的时候降序排序。

TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
System.out.println(treeSet); // [1, 2, 3, 4]

TreeSet 可以接收 Comparator 对象作为构造参数,用于自定义排序规则。

// 简化的 Lambda 表达式写法
// TreeSet<Integer> treeSet = new TreeSet<>((o1, o2) -> o2 - o1);
TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});

treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
System.out.println(treeSet); // [4, 3, 2, 1]

HashSet(使用最多)

HashSet 比较对象时,要保证两个对象数据唯一,必须同时重写 equalshashCode 方法

class Student {
  //...
  // IDEA 中 Alt + Insert 快捷键生成
  @Override
  public boolean equals(Object o) {
      if (o == null || getClass() != o.getClass()) return false;
      Student student = (Student) o;
      return Objects.equals(name, student.name) && Objects.equals(age, student.age);
  }

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

HashSet<Student> hashSet = new HashSet<>();

hashSet.add(new Student("张三", 20));
hashSet.add(new Student("李四", 22));
hashSet.add(new Student("王五", 18));
hashSet.add(new Student("王五", 18));

// 最后一条数据被去重了
System.out.println(hashSet); // [Student{name='张三', age=20}, Student{name='王五', age=18}, Student{name='李四', age=22}]

HashSet 原理:

为什么当 hashCode 相同时,还需要再调用 equals 方法进行对比?因为存在 hash 碰撞

以下例子,重地通话 不是相同内容,但是 hash 值却相同,因此需要 equals 来做二次对比。

System.out.println("你好".hashCode()); // 652829
System.out.println("重地".hashCode()); // 1179395
System.out.println("通话".hashCode()); // 1179395

LinkedHashSet

唯一一个可以保证存取顺序的集合,内部使用链表结构。同样的,再对比对象时想要去重,和 HashSet 一样,必须同时重写 equalshashCode 方法

Collections 工具类

可变参数(js args)

类似 js 的 arguments 写法:sums(...args),args 本质是一个类数组。

sums(1);
sums(1, 2);
sums(1, 2, 3);
sums(1, 2, 3, 4);

public static int sums(int... nums) {
    int total = 0;
    for (int num : nums) {
        total += num;
    }
    return total;
}

常用工具类

求最大、最小值,需要对象实现 Comparable 接口,否则无法比较,报错。

ArrayList<Integer> list = new ArrayList<>();
// 批量添加
Collections.addAll(list, 3, 1, 6, 9, 5, 7);
System.out.println(list); // [3, 1, 6, 9, 5, 7]

// 洗牌
Collections.shuffle(list);
System.out.println(list); // [5, 1, 6, 7, 3, 9]

// 最值,对象需要实现 Comparable 接口
System.out.println(Collections.max(list)); // 9
System.out.println(Collections.min(list)); // 1

// 排序(默认自然排序)
Collections.sort(list);
System.out.println(list); // [1, 3, 5, 6, 7, 9]
// 自定义排序规则(降序)
Collections.sort(list, (o1, o2) -> o2 - o1);
System.out.println(list); // [9, 7, 6, 5, 3, 1]

Map



上一篇
vue-element-admin 迁移至 rsbuild v1