头图

之前的100道Go的面试题和答案反馈不错,我又整理了121道Java的常见面试题,有需要的朋友可以点赞收藏一下。

干货太多了,一篇都装不下,今天先发60个

1. Java是什么?⭐⭐⭐

Java 是一种面向对象的编程语言和计算平台,最早由 Sun Microsystems 于 1995 年发布,后来被 Oracle 公司收购。Java 被广泛用于开发各种应用程序,从桌面应用到企业级服务器和移动应用。

java 具有以下特点:

1、平台无关性

Java 的口号是“编写一次,运行到处”(Write Once, Run Anywhere)。这主要是通过 Java 虚拟机(JVM)实现的,JVM 是一个可以在任何支持 Java 的平台上运行 Java 字节码的虚拟机。Java 源代码编译成字节码后,可以在任何安装了 JVM 的平台上运行,无需重新编译。

2、面向对象

Java 是一种面向对象的编程语言,支持封装、继承和多态等面向对象的基本概念。面向对象编程使代码更具模块化、可重用性和可维护性。

3、简单

Java 的语法与 C++ 类似,但去掉了 C++ 中一些复杂和容易出错的特性(如指针算术和多重继承),使其更简单易学。

4、内存管理

Java 使用自动垃圾回收机制(Garbage Collection)来管理内存。这意味着程序员不需要手动释放内存,减少了内存泄漏和其他内存管理错误的可能性。

5、 多线程

Java 内置了对多线程编程的支持,使得开发并发应用程序更加容易。Java 提供了丰富的线程 API 和高级并发工具类(如java.util.concurrent包)。

6、 广泛的应用领域

Java 被广泛应用于各个领域,包括:

企业级应用:Java EE(Enterprise Edition)提供了开发企业级应用的标准平台。

移动应用:Android 应用程序主要使用 Java 编写。

Web 应用:Java 提供了多种框架和工具(如 Spring、Hibernate)来开发 Web 应用。

嵌入式系统:Java 也可以用于开发嵌入式系统和物联网设备。

7、社区和生态系统

Java 拥有一个庞大而活跃的开发者社区和丰富的生态系统,提供了大量的开源库、框架和工具,帮助开发者快速构建高质量的应用程序。

2. 介绍一下常见的list实现类⭐⭐⭐⭐⭐

ArrayList

ArrayList 是最常用的 List 实现类,线程不安全,内部是通过数组实现的,继承了AbstractList,实现了List。它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。排列有序,可重复,容量不够的时候,新容量的计算公式为:

newCapacity = oldCapacity + (oldCapacity >> 1),这实际上是将原容量增加50%(即乘以1.5)。

ArrayList实现了RandomAccess接口,即提供了随机访问功能。RandomAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

ArrayList实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

LinkedList(链表)

LinkedList 是用链表结构存储数据的,线程不安全。很适合数据的动态插入和删除,随机访问和遍历速度比较慢,增删快。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。底层使用双向链表数据结构。

LinkedList是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

Vector(数组实现、线程同步)

Vector 与 ArrayList 一样,也是通过数组实现的,Vector和ArrayList用法上几乎相同,但Vector比较古老,一般不用。Vector是线程同步的,效率低。即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。默认扩展一倍容量。

3. ArrayList初始容量是多少?⭐⭐⭐⭐⭐

ArrayList 是 Java 中用于动态数组的一个类。它可以在添加或删除元素时自动调整其大小。然而,ArrayList 有一个默认的初始容量,这个容量是在你创建 ArrayList 实例时如果没有明确指定容量参数时所使用的。

在 Java 的 ArrayList 实现中,默认的初始容量是 10。这意味着当你创建一个新的 ArrayList 而不指定其容量时,它会以一个内部数组长度为 10 的数组来开始。当添加的元素数量超过这个初始容量时,ArrayList 的内部数组会进行扩容,通常是增长为原来的 1.5 倍。

ArrayList<String> list = new ArrayList<>(); // 默认的初始容量是 10

但是,如果你知道你将要在 ArrayList 中存储多少元素,或者预计会存储多少元素,那么最好在创建时指定一个初始容量,这样可以减少由于扩容而导致的重新分配数组和复制元素的操作,从而提高性能。

ArrayList<String> list = new ArrayList<>(50); // 初始容量设置为 50

自从1.7之后,arraylist初始化的时候为一个空数组。但是当你去放入第一个元素的时候,会触发他的懒加载机制,使得数量变为10。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);        
}        
return minCapacity;    
}

所以我们的arraylist初始容量的确是10。只不过jdk8变为懒加载来节省内存。进行了一点优化。

所以我们的arraylist初始容量的确是10。只不过jdk8变为懒加载来节省内存。进行了一点优化。

4. JVM、JRE和JDK之间的关系⭐⭐⭐

JVM、JRE 和 JDK 是 Java 生态系统中的三个核心组件,它们在 Java 开发和运行时环境中扮演着不同的角色。

JVM (Java Virtual Machine)

Java 虚拟机是一种抽象计算机,它为 Java 程序提供了运行环境。JVM 的主要职责是执行 Java 字节码,并将其转换为机器代码,以便在特定平台上运行。JVM 是实现 Java 平台无关性的关键组件。

主要负责:字节码执行:JVM 负责加载、验证和执行 Java 字节码。内存管理:JVM 管理堆内存和栈内存,并执行垃圾回收(Garbage Collection)。安全性:JVM 提供了安全机制,确保 Java 应用在受控环境中运行。

JRE (Java Runtime Environment)

Java 运行时环境是一个软件包,它提供了运行 Java 应用程序所需的所有组件。JRE 包含 JVM 以及 Java 类库和其他支持文件。JRE 是运行 Java 应用程序的最低要求。JRE 包含 JVM,用于执行 Java 字节码。JRE 包含 Java 标准类库(如java.lang、java.util等),这些类库为 Java 应用提供基础功能。

JDK (Java Development Kit)

Java 开发工具包是为 Java 开发者提供的完整开发环境。JDK 包含 JRE 以及开发 Java 应用程序所需的工具和库。JDK 是开发和编译 Java 程序的必备工具。JDK 包含一个完整的 JRE 环境。包括编译器(javac)、调试器(jdb)、打包工具(jar)等,用于开发、编译和调试 Java 程序。提供了额外的库和头文件,用于开发 Java 应用程序。

关系

JVM 是 JRE 的一部分:JVM 是 JRE 中的核心组件,负责执行 Java 字节码。

JRE 是 JDK 的一部分:JRE 提供了运行 Java 应用程序所需的环境,而 JDK 则在此基础上添加了开发工具和额外的库。

图示关系

JDK

├── JRE

│ ├── JVM

│ ├── 核心类库

│ └── 其他支持文件

├── 开发工具(javac、jdb、jar 等)

└── 额外的库和头文件

5. ArrayList是如何扩容的?⭐⭐⭐⭐⭐

ArrayList的扩容机制是Java集合框架中一个重要的概念,它允许ArrayList在需要时自动增加其内部数组的大小以容纳更多的元素。主要有以下的步骤

1、初始容量和扩容因子:

当创建一个新的ArrayList对象时,它通常会分配一个初始容量,这个初始容量默认为10。

ArrayList的扩容因子是一个用于计算新容量的乘数,默认为1.5。

2、扩容触发条件:

当向ArrayList中添加一个新元素,并且该元素的数量超过当前数组的容量时,就会触发扩容操作。

3、扩容策略:

扩容时,首先根据当前容量和扩容因子计算出一个新的容量。新容量的计算公式为:

newCapacity = oldCapacity + (oldCapacity >> 1),这实际上是将原容量增加50%(即乘以1.5)。

如果需要的容量大于Integer.MAX\_VALUE - 8(因为数组的长度是一个int类型,其最大值是Integer.MAX\_VALUE,但ArrayList需要预留一些空间用于内部操作),则会使用Integer.MAX\_VALUE作为新的容量。

4、扩容过程:

创建一个新的数组,其长度为新计算的容量。

将原数组中的所有元素复制到新数组中。

将ArrayList的内部引用从原数组更新为新数组。

将新元素添加到新数组的末尾。

5、性能影响:

扩容过程涉及到内存分配和元素复制,可能会对性能产生一定的影响。因此,在使用ArrayList时,如果可能的话,最好预估需要存储的元素数量,并设置一个合适的初始容量,以减少扩容的次数。

6. Java的三大特性⭐⭐⭐⭐⭐

Java 编程语言的三大特性是面向对象编程(OOP)的核心概念:封装、继承和多态。这些特性使得 Java 程序具有良好的结构和可维护性。

封装(Encapsulation)

封装是将对象的状态(属性)和行为(方法)组合在一起,并对外隐藏对象的内部细节,只暴露必要的接口。通过封装,可以保护对象的状态不被外部直接修改,增强了代码的安全性和可维护性。

使用private关键字将属性声明为私有的。

提供public的 getter 和 setter 方法来访问和修改私有属性。

示例

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

    // Getter 方法
    public String getName() {
        return name;
    }

    // Setter 方法
    public void setName(String name) {
        this.name = name;
    }

    // Getter 方法
    public int getAge() {
        return age;
    }

    // Setter 方法
    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        }
    }
}

继承(Inheritance)

继承是面向对象编程中的一个机制,通过继承,一个类可以继承另一个类的属性和方法,从而实现代码的重用。被继承的类称为父类(超类),继承的类称为子类(派生类)。主要使用extends关键字来声明一个类继承另一个类。

示例

public class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("The dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // 调用继承自 Animal 类的方法
        myDog.bark(); // 调用 Dog 类的方法
    }
}

多态(Polymorphism)

多态是指同一个方法在不同对象中具有不同的实现方式。多态性允许对象在不同的上下文中以不同的形式表现。多态可以通过方法重载(Overloading)和方法重写(Overriding)来实现。

方法重载:在同一个类中,方法名相同但参数列表不同。

方法重写:在子类中重新定义父类中的方法。

示例

// 方法重载
public class MathUtils {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
}

// 方法重写
public class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal();
        Animal myDog = new Dog();

        myAnimal.makeSound(); // 输出: Some generic animal sound
        myDog.makeSound();    // 输出: Bark
    }
}

7. ArrayList的添加与删除元素为什么慢?⭐⭐⭐⭐⭐

主要是由于其内部实现基于数组的特性所导致的。

ArrayList的添加与删除操作慢,主要是因为其内部实现基于数组,而数组在插入和删除元素时需要移动其他元素来保证连续性和顺序性,这个过程需要耗费较多的时间。

相对于基于链表的数据结构(如LinkedList),ArrayList的插入和删除操作的时间复杂度是O(n)级别的,而链表的时间复杂度为O(1)。

添加元素

尾部添加

当在ArrayList的尾部添加元素时,如果当前数组的容量还未达到最大值,只需要将新元素添加到数组的末尾即可,此时时间复杂度为O(1)。

但是,当数组容量已满时,需要进行扩容操作。扩容操作通常会将数组的容量增加到当前容量的1.5倍或2倍,并将原数组中的所有元素复制到新的更大的数组中。这一过程的时间复杂度为O(n),其中n为当前数组中的元素数量。

指定位置插入

当在ArrayList的指定位置(非尾部)插入元素时,需要将目标位置之后的所有元素向后移动一个位置,然后将新元素插入到指定位置。这个过程涉及到移动元素的操作,时间复杂度为O(n),在最坏情况下,如头部插入,需要移动所有的元素。

删除元素

尾部删除

当删除的元素位于列表末尾时,只需要将末尾元素移除即可,时间复杂度为O(1)。

指定位置删除

当在ArrayList的指定位置(非尾部)删除元素时,需要将删除点之后的所有元素向前移动一个位置,以填补被删除元素的位置。这个过程同样涉及到移动元素的操作,时间复杂度为O(n),在最坏情况下,如头部删除,需要移动除了被删除元素之外的所有元素。

8. 什么是封装?⭐⭐⭐⭐⭐

封装(Encapsulation)是面向对象编程(OOP)中的一个基本概念。它涉及到将对象的状态(属性)和行为(方法)封装在一个类中,并对外部隐藏内部实现细节,只暴露必要的接口。这种做法有助于提高代码的安全性、可维护性和可重用性。

重点特性:

属性私有化:将类的属性声明为私有(private),以防止外部直接访问和修改这些属性。

提供公共的访问方法:通过公共(public)的 getter 和 setter 方法来控制对私有属性的访问和修改。这些方法允许外部代码在受控的情况下读取和修改属性值。

隐藏实现细节:封装还包括隐藏类内部的实现细节,只暴露必要的接口给外部使用者。这有助于降低代码的复杂性,提高模块化程度。

封装的优点

  • 数据保护:通过私有化属性,可以防止外部代码直接修改对象的状态,从而保护数据的完整性。
  • 简化接口:只暴露必要的方法,隐藏不需要的实现细节,使得类的接口更加简洁明了。
  • 提高可维护性:封装使得类的实现细节可以独立于外部代码进行修改,只要接口不变,外部代码不需要做任何改变。
  • 增强灵活性:通过 getter 和 setter 方法,可以在访问或修改属性时添加额外的逻辑,比如数据验证或事件触发。

示例

public class Person {
    // 私有属性
    private String name;
    private int age;

    // 公共的 getter 方法,用于获取 name 的值
    public String getName() {
        return name;
    }

    // 公共的 setter 方法,用于设置 name 的值
    public void setName(String name) {
        this.name = name;
    }

    // 公共的 getter 方法,用于获取 age 的值
    public int getAge() {
        return age;
    }

    // 公共的 setter 方法,用于设置 age 的值
    public void setAge(int age) {
        if (age > 0) { // 添加数据验证逻辑
            this.age = age;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();

        // 使用 setter 方法设置属性值
        person.setName("John Doe");
        person.setAge(30);

        // 使用 getter 方法获取属性值
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());
    }
}

9. ArrayList是线程安全的吗?⭐⭐⭐⭐⭐

ArrayList不是线程安全的。在多线程环境下,如果多个线程同时对ArrayList进行操作,可能会出现数据不一致的情况。

当多个线程同时对ArrayList进行添加、删除等操作时,可能会导致数组大小的变化,从而引发数据不一致的问题。例如,当一个线程在对ArrayList进行添加元素的操作时(这通常分为两步:先在指定位置存放元素,然后增加size的值),另一个线程可能同时进行删除或其他操作,导致数据的不一致或错误。

比如下面的这个代码,就是实际上ArrayList 放入元素的代码:

Java elementData[size] = e; size = size + 1;

  1. elementData[size] = e; 这一行代码是将新的元素 e 放置在 ArrayList 的内部数组 elementData 的当前大小 size 的位置上。这里假设 elementData 数组已经足够大,可以容纳新添加的元素(实际上 ArrayList 在必要时会增长数组的大小)。
  2. size = size + 1; 这一行代码是更新 ArrayList 的大小,使其包含新添加的元素。

如果两个线程同时尝试向同一个 ArrayList 实例中添加元素,那么可能会发生以下情况:

• 线程 A 执行 elementData[size] = eA;(假设当前 size 是 0)

• 线程 B 执行 elementData[size] = eB;(由于线程 A 尚未更新 size,线程 B 看到的 size 仍然是 0)

• 此时,elementData[0] 被线程 B 的 eB 覆盖,线程 A 的 eA 丢失

• 线程 A 更新 size = 1;

• 线程 B 更新 size = 1;(现在 size 仍然是 1,但是应该是 2,因为有两个元素被添加)

为了解决ArrayList的线程安全问题,可以采取以下几种方式:

  1. 使用Collections类的synchronizedList方法:将ArrayList转换为线程安全的List。这种方式通过在对ArrayList进行操作时加锁来保证线程安全,但可能会带来一定的性能损耗。
  2. 使用CopyOnWriteArrayList类:它是Java并发包中提供的线程安全的List实现。CopyOnWriteArrayList在对集合进行修改时,会创建一个新的数组来保存修改后的数据,这样就不会影响到其他线程对原数组的访问。因此,它适合在读操作远远多于写操作的场景下使用。
  3. 使用并发包中的锁机制:如Lock或Semaphore等,显式地使用锁来保护对ArrayList的操作,可以确保在多线程环境下数据的一致性。但这种方式需要开发人员自行管理锁的获取和释放,容易出现死锁等问题。

还可以考虑使用其他线程安全的集合类,如Vector或ConcurrentLinkedQueue等,它们本身就是线程安全的,可以直接在多线程环境下使用。

10. 什么是继承?⭐⭐⭐⭐⭐

继承(Inheritance)是面向对象编程(OOP)中的一个核心概念。它允许一个类(子类或派生类)继承另一个类(父类或超类)的属性和方法,从而实现代码的重用和扩展。通过继承,子类可以复用父类的代码,并且可以新增或重写(覆盖)父类的方法以实现特定的功能。

继承的基本概念

父类(Super Class):被继承的类,提供属性和方法。

子类(Sub Class):继承父类的类,可以复用父类的代码,并且可以新增或重写父类的方法。

extends关键字:用于声明一个类继承另一个类。

继承的特点

单继承:Java 只支持单继承,即一个类只能有一个直接父类。

继承层次:子类可以继续被其他类继承,形成继承层次结构。

super关键字:用于引用父类的属性和方法,特别是在子类重写父类的方法时,可以通过super调用父类的方法。

继承的优点

代码重用:子类可以复用父类的代码,减少代码重复。

代码扩展:子类可以在继承父类的基础上新增属性和方法,扩展功能。

多态性:通过继承和方法重写,可以实现多态性,使得同一方法在不同对象中具有不同的实现。

示例

// 定义父类 Animal
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }

    public String getName() {
        return name;
    }

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

// 定义子类 Dog,继承自 Animal
public class Dog extends Animal {
    public Dog(String name) {
        super(name); // 调用父类的构造方法
    }

    // 子类特有的方法
    public void bark() {
        System.out.println(getName() + " is barking.");
    }

    // 重写父类的方法
    @Override
    public void eat() {
        System.out.println(getName() + " is eating dog food.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog("Buddy");

        // 调用继承自父类的方法
        myDog.eat(); // 输出: Buddy is eating dog food.

        // 调用子类特有的方法
        myDog.bark(); // 输出: Buddy is barking.
    }
}

继承中的一些注意事项

  1. 构造方法:子类的构造方法会调用父类的构造方法。如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法。
  2. 方法重写(Overriding):子类可以重写父类的方法,以提供特定的实现。重写的方法必须具有相同的方法签名(方法名、参数列表和返回类型)。
  3. super关键字:用于调用父类的构造方法或父类的方法。例如,在子类的构造方法中使用super调用父类的构造方法。

11. ArrayList如何保证线程安全?⭐⭐⭐⭐⭐

借助锁

可以通过在访问 ArrayList 的代码块上使用 synchronized 关键字来手动同步对 ArrayList 的访问。这要求所有访问 ArrayList 的代码都知道并使用相同的锁。

使用 Collections.synchronizedList

Collections.synchronizedList 方法返回一个线程安全的列表,该列表是通过在每个公共方法(如 add(), get(), iterator() 等)上添加同步来实现的,其中同步是基于里面的同步代码块实现。但是,和手动同步一样,它也不能解决在迭代过程中进行结构修改导致的问题。

Java List<String> list = Collections.synchronizedList(new ArrayList<>());

使用并发集合

Java 并发包 java.util.concurrent 提供了一些线程安全的集合类,如 CopyOnWriteArrayList。这些类提供了不同的线程安全保证和性能特性。

CopyOnWriteArrayList是一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行新的复制来实现的。因此,迭代器不会受到并发修改的影响,并且遍历期间不需要额外的同步。但是,当有很多写操作时,这种方法可能会很昂贵,因为它需要在每次修改时复制整个底层数组。

Java List<String> list = new CopyOnWriteArrayList<>();

选择解决方案时,需要考虑并发模式、读写比例以及性能需求。如果你的应用主要是读操作并且偶尔有写操作,CopyOnWriteArrayList是一个好选择。如果你的应用有大量的写操作,那么可能需要使用其他并发集合或手动同步策略。

12. 什么是多态?⭐⭐⭐⭐⭐

多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它允许相同的操作在不同的对象上表现出不同的行为。多态性使得一个接口可以有多种实现,从而提高代码的灵活性和可扩展性。

多态的类型

多态主要有两种形式:

  1. 编译时多态(静态多态):通过方法重载(Method Overloading)实现。
  2. 运行时多态(动态多态):通过方法重写(Method Overriding)和接口实现(Interface Implementation)实现。

编译时多态(方法重载)

编译时多态是通过方法重载实现的,即同一个类中多个方法具有相同的名称,但参数列表不同。编译器在编译时根据方法的参数列表来决定调用哪个方法。

示例

public class MathUtils {
    // 方法重载:add 方法有两个版本
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        MathUtils utils = new MathUtils();
        System.out.println(utils.add(2, 3));       // 输出: 5
        System.out.println(utils.add(2.0, 3.0));   // 输出: 5.0
    }
}

运行时多态(方法重写)

运行时多态是通过方法重写实现的,即子类重写父类的方法。在运行时,Java 虚拟机根据对象的实际类型调用对应的方法。

示例

// 父类classAnimal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}
// 子类class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat meows");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal myAnimal=new Dog(); // 父类引用指向子类对象
        myAnimal.makeSound();        // 输出: Dog barks

        myAnimal = new Cat();        // 父类引用指向另一个子类对象
        myAnimal.makeSound();        // 输出: Cat meows
    }
}

接口和抽象类的多态性

多态性还可以通过接口和抽象类实现。子类或实现类可以提供不同的实现,从而实现多态性。

示例

// 定义接口interface Shape {
    void draw();
}
// 实现接口的类class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}
class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Rectangle");
    }
}
public class Main {
    public static void main(String[] args) {
        Shape shape1=new Circle();    // 接口引用指向实现类对象
        Shape shape2=new Rectangle(); // 接口引用指向另一个实现类对象

        shape1.draw(); // 输出: Drawing a Circle
        shape2.draw(); // 输出: Drawing a Rectangle
    }
}

多态的优点

  1. 代码重用:通过多态性,可以使用同一个接口或父类来操作不同的对象,减少代码重复。
  2. 灵活性和可扩展性:多态性使得代码更加灵活,可以轻松地扩展新的子类或实现类而不影响现有代码。
  3. 简化代码:通过多态性,可以使用统一的接口来处理不同的对象,简化代码逻辑。

13. 构造器是否可被重写?⭐⭐

构造器不能被重写:因为构造器不属于类的继承成员,并且它们的名称必须与类名相同。

构造器可以被重载:在同一个类中,可以定义多个构造器,只要它们的参数列表不同。

构造器不能被重写

构造器不能被重写(Overridden)。重写是指在子类中提供一个与父类方法具有相同签名的方法,以便在子类中提供该方法的具体实现。但构造器不属于类的继承成员,因此不能被子类重写。

原因

  1. 构造器的作用:构造器的主要作用是初始化对象的状态。每个类都有自己的构造器,用于初始化该类的实例。子类不能直接继承父类的构造器,因为子类的初始化过程可能与父类不同。
  2. 方法签名不同:重写要求方法签名(包括方法名称和参数列表)相同,而构造器在子类中的名称与父类不同(构造器名称必须与类名相同)。
  3. 构造器不是类成员:构造器不属于类的成员方法,它们是特殊的初始化方法,不参与继承机制。

构造器可以被重载

虽然构造器不能被重写,但它们可以被重载(Overloaded)。构造器重载是指在同一个类中定义多个构造器,这些构造器具有相同的名称(类名),但参数列表不同。

示例

public class Parent {
    public Parent() {
        System.out.println("Parent default constructor");
    }

    public Parent(String message) {
        System.out.println("Parent constructor with message: " + message);
    }
}

public class Child extends Parent {
    public Child() {
        super(); // 调用父类的默认构造器
        System.out.println("Child default constructor");
    }

    public Child(String message) {
        super(message); // 调用父类的带参数构造器
        System.out.println("Child constructor with message: " + message);
    }
}

在上述示例中,Parent类有两个构造器,一个是默认构造器,另一个是带参数的构造器。Child类也定义了两个构造器,并在其中调用了父类的相应构造器。

14. 聊聊常见set类都有哪几种?⭐⭐⭐⭐⭐

在Java中,Set是一种不包含重复元素的集合,它继承自Collection接口。用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。Java中常见的Set实现类主要有三个:HashSet、LinkedHashSet和TreeSet。

HashSet

HashSet是Set接口的一个实现类,它基于哈希表实现,具有快速的插入、删除和查找操作。

HashSet不保证元素的迭代顺序,允许null元素的存在,但只能有一个null元素,不是线程安全的,如果多个线程同时访问并修改HashSet,则需要外部同步。

当存储自定义对象时,需要重写对象的hashCode()和equals()方法,以确保对象的唯一性。

LinkedHashSet

LinkedHashSet是HashSet的子类,它基于哈希表和双向链表实现,继承与 HashSet、又基于 LinkedHashMap 来实现的。可以维护元素的插入顺序。相比于 HashSet 增加了顺序性。

其主要是将元素按照插入的顺序进行迭代,同时继承了HashSet的所有特性。因此当需要保持元素插入顺序时,可以选择使用LinkedHashSet。

TreeSet

TreeSet是Set接口的另一个实现类,它基于红黑树实现,可以对元素进行自然排序或自定义排序。

可以实现元素按照升序进行排序(自然排序或自定义排序)。不过它不允许null元素的存在。treeset 同样是线程不安全的。

当存储自定义对象时,如果想要进行排序,需要实现Comparable接口并重写compareTo()方法,或提供自定义的Comparator对象来进行排序。

适用场景

HashSet:适用于需要快速查找的场景,不保证元素的顺序。

LinkedHashSet:适用于需要保持元素插入顺序的场景。

TreeSet:适用于需要元素排序的场景。

15. Hashset的底层原理?⭐⭐⭐⭐⭐

HashSet是 Java 中一个常用的集合类,它用于存储不重复的元素。HashSet的底层实现依赖于HashMap。

基本结构

HashSet底层使用HashMap来存储元素。具体来说,每当你向HashSet中添加一个元素时,这个元素实际上是作为HashMap的键来存储的,而HashMap的值是一个固定的常量对象。

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
    private transient HashMap<E,Object> map;

    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }

    // 其他构造函数和方法
}

添加元素 (add方法)

当你调用HashSet的add方法时,HashSet会将元素作为键插入到HashMap中,值为一个常量对象PRESENT。

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

HashMap的put方法会检查键是否已经存在,如果不存在则插入新键值对。如果键已经存在,则更新键值对并返回旧值。因此,HashSet能够保证元素唯一性。

元素查找 (contains方法)

HashSet的contains方法实际上是调用HashMap的containsKey方法。

public boolean contains(Object o) {
    return map.containsKey(o);
}

HashMap的containsKey方法通过计算键的hashCode值来快速定位元素的位置,然后进行比较。

删除元素 (remove方法)

HashSet的remove方法调用HashMap的remove方法来删除元素。

public boolean remove(Object o) {
    return map.remove(o) == PRESENT;
}

16. String、StringBuffer 和 StringBuilder 的区别⭐⭐⭐⭐⭐

String

不可变性:String对象是不可变的。一旦创建,字符串的内容就不能被改变。任何对字符串的修改都会生成一个新的String对象。

线程安全:由于String对象是不可变的,它们是线程安全的,可以在多个线程中安全地共享。

适用于字符串内容不会发生变化的场景,例如字符串常量、少量的字符串操作等。

Java String str = "Hello"; str = str + " World"; // 生成一个新的字符串对象

StringBuffer

可变性:StringBuffer对象是可变的,可以对字符串内容进行修改,而不会生成新的对象。

线程安全:StringBuffer是线程安全的,它的方法是同步的,可以在多线程环境中安全使用。

适用于在多线程环境中需要频繁修改字符串内容的场景。

Java StringBuffer sb = new StringBuffer("Hello"); sb.append(" World"); // 修改同一个 StringBuffer 对象

StringBuilder

可变性:StringBuilder对象也是可变的,可以对字符串内容进行修改,而不会生成新的对象。

非线程安全:StringBuilder不是线程安全的,它的方法没有同步,因此在多线程环境中使用时需要额外注意。

适用于在单线程环境中需要频繁修改字符串内容的场景,性能比StringBuffer更高。

Java StringBuilder sb = new StringBuilder("Hello"); sb.append(" World"); // 修改同一个 StringBuilder 对象

选择哪个类取决于具体的使用场景。如果字符串内容不变,使用String;如果需要在多线程环境中修改字符串,使用StringBuffer;如果在单线程环境中修改字符串,使用StringBuilder。

17. HashSet如何实现线程安全?⭐⭐⭐⭐⭐

HashSet本身不是线程安全的。如果多个线程在没有外部同步的情况下同时访问一个HashSet,并且至少有一个线程修改了集合,那么它必须保持同步。

使用Collections.synchronizedSet

Java 提供了一个简单的方法来创建一个同步的集合,通过Collections.synchronizedSet方法。这个方法返回一个线程安全的集合包装器。

Set<String> synchronizedSet = Collections.synchronizedSet(newHashSet<>());

使用这个方法后,所有对集合的访问都将是同步的。但是,需要注意的是,对于迭代操作,必须手动同步:

Set<String> synchronizedSet = Collections.synchronizedSet(newHashSet<>());
synchronized (synchronizedSet) {
    Iterator<String> iterator = synchronizedSet.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

使用ConcurrentHashMap

如果需要更高效的并发访问,可以使用ConcurrentHashMap来实现类似HashSet的功能。ConcurrentHashMap提供了更细粒度的锁机制,在高并发环境下性能更好。

Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

ConcurrentHashMap.newKeySet()返回一个基于ConcurrentHashMap的Set实现,它是线程安全的,并且在高并发环境下性能优越。 使用CopyOnWriteArraySet 对于读操作远多于写操作的场景,可以使用CopyOnWriteArraySet。它的实现基于CopyOnWriteArrayList,在每次修改时都会复制整个底层数组,因此在写操作较少时性能较好。

Set<String> copyOnWriteArraySet = newCopyOnWriteArraySet<>();

手动同步

如果你不想使用上述任何一种方法,也可以手动同步HashSet的访问。可以使用synchronized关键字来保护对HashSet的访问:

Set<String> hashSet = newHashSet<>();
synchronized (hashSet) {
    // 对 hashSet 的操作
}

选择合适的方案

如果你的应用程序是单线程的,或只有少量的线程访问集合,可以使用Collections.synchronizedSet。

如果你的应用程序有大量的并发读写操作,可以使用ConcurrentHashMap.newKeySet。

如果你的应用程序读操作远多于写操作,可以使用CopyOnWriteArraySet。

代码 Demo

再给大家弄一个使用ConcurrentHashMap实现线程安全Set的示例代码:

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashSetExample {
    public static void main(String[] args) {
        Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

        // 多线程环境下的操作示例
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                concurrentSet.add(Thread.currentThread().getName() + "-" + i);
            }
        };

        Thread thread1 = new Thread(task, "Thread1");
        Thread thread2 = new Thread(task, "Thread2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Set size: " + concurrentSet.size());
    }
}

18. 字符串常量拼接的过程 (jdk1.8)⭐⭐

编译时优化

编译时常量折叠

对于编译时已知的字符串常量,Java 编译器会进行常量折叠(Constant Folding)。这意味着在编译阶段,编译器会直接计算出拼接结果,并将其作为一个单一的字符串常量存储在.class文件中。例如:在编译时,这段代码会被优化为:

Java String str = "Hello" + " " + "World";

Java String str = "Hello World";

非常量表达式:

如果拼接的字符串包含变量或方法调用,编译器不能在编译时确定结果,因此需要在运行时进行拼接。在 JDK 1.8 中,编译器会将这些拼接操作转换为使用StringBuilder的代码。例如:在编译时,这段代码会被转换为:

Java String str1 = "Hello"; String str2 = "World"; String result = str1 + " " + str2;

Java StringBuilder sb = new StringBuilder(); sb.append(str1); sb.append(" "); sb.append(str2); String result = sb.toString();

运行时处理

StringBuilder的使用

在运行时,对于非常量的字符串拼接,StringBuilder被用来构建最终的字符串。StringBuilder是可变的,因此可以高效地进行字符串的拼接操作。

例如:在运行时,StringBuilder会依次调用append方法,将各个部分拼接起来,并最终调用toString方法生成结果字符串。

Java String str1 = "Hello"; String str2 = "World"; String result = str1 + " " + str2;

性能优化

使用StringBuilder进行拼接比直接使用String的+操作符效率高得多,因为String是不可变的,每次拼接都会创建新的String对象,而StringBuilder则是可变的,可以在原有对象上进行修改。

19. 抽象类和接口的区别⭐⭐⭐⭐⭐

定义方式

接口:使用interface关键字定义。不能包含实例变量,只能包含常量(public static final)。方法默认是public和abstract的(Java 8之后可以有默认方法和静态方法)。不能有构造器。

public interface MyInterface {
    void method1(); // 抽象方法
    default void method2() { // 默认方法 (Java 8+)
        System.out.println("Default method");
    }
    static void method3() { // 静态方法 (Java 8+)
        System.out.println("Static method");
    }
}

抽象类:使用abstract关键字定义。可以包含实例变量和常量。可以包含抽象方法和具体方法(有方法体的)。可以有构造器。

public abstract class MyAbstractClass {
    private int value;
    
    public MyAbstractClass(int value) {
        this.value = value;
    }
    
    public abstract void method1(); // 抽象方法
    
    public void method2() { // 具体方法
        System.out.println("Concrete method");
    }
}

继承和实现

接口:一个类可以实现多个接口(多重继承)。接口可以继承多个其他接口。

public class MyClass implements MyInterface, AnotherInterface {
    @Override
    public void method1() {
        // 实现方法
    }
}

public interface AnotherInterface extends MyInterface {
    void anotherMethod();
}

抽象类:一个类只能继承一个抽象类(单继承)。抽象类可以继承其他类和实现接口。

public class MyClass extends MyAbstractClass {
    public MyClass(int value) {
        super(value);
    }
    
    @Override
    public void method1() {
        // 实现抽象方法
    }
}

使用场景

接口:用于定义一组不相关类的公共行为。适合用于API设计,提供灵活的多重继承能力。更适合定义能力(能力接口),例如Comparable、Serializable。

抽象类:用于定义一组相关类的公共行为。适合用于提供基础实现和共享代码。更适合定义类之间的层次结构,提供公共的实现和状态。

实现细节

接口:不包含实现细节(除了Java 8引入的默认方法和静态方法)。

抽象类:可以包含部分实现细节,允许子类继承和重用。

20. 介绍一下HashMap?⭐⭐⭐⭐⭐

HashMap主要是用于存储键值对。它是基于哈希表实现的,提供了快速的插入、删除和查找操作。从安全角度,HashMap不是线程安全的。如果多个线程同时访问一个HashMap并且至少有一个线程修改了它,则必须手动同步。

HashMap允许一个 null 键和多个 null 值。

HashMap不保证映射的顺序,特别是它不保证顺序会随着时间的推移保持不变。

HashMap提供了 O(1) 时间复杂度的基本操作(如 get 和 put),前提是哈希函数的分布良好且冲突较少。

HashMap主要方法

put(K key, V value):将指定的值与该映射中的指定键关联。如果映射以前包含一个该键的映射,则旧值将被替换。

get(Object key):返回指定键所映射的值;如果此映射不包含该键的映射,则返回 null。

remove(Object key):如果存在一个键的映射,则将其从映射中移除。

containsKey(Object key):如果此映射包含指定键的映射,则返回 true。

containsValue(Object value):如果此映射将一个或多个键映射到指定值,则返回 true。

size():返回此映射中的键值映射关系的数量。

isEmpty():如果此映射不包含键值映射关系,则返回 true。

clear():从此映射中移除所有键值映射关系。

代码 Demo

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

public class HashMapExample {
    public static void main(String[] args) {
        // 创建一个 HashMap 实例
        Map<String, Integer> map = new HashMap<>();

        // 添加键值对
        map.put("apple", 1);
        map.put("banana", 2);
        map.put("orange", 3);

        // 访问元素
        System.out.println("Value for key 'apple': " + map.get("apple"));

        // 检查是否包含某个键或值
        System.out.println("Contains key 'banana': " + map.containsKey("banana"));
        System.out.println("Contains value 3: " + map.containsValue(3));

        // 移除元素
        map.remove("orange");

        // 遍历 HashMap
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }

        // 获取大小
        System.out.println("Size of map: " + map.size());

        // 清空 HashMap
        map.clear();
        System.out.println("Is map empty: " + map.isEmpty());
    }
}

内部工作原理

HashMap使用哈希表来存储数据。哈希表是基于数组和链表的组合结构。

  1. 哈希函数:HashMap使用键的hashCode()方法来计算哈希值,然后将哈希值映射到数组的索引位置。
  2. 数组和链表:HashMap使用一个数组来存储链表或树结构(Java 8 及以后)。每个数组位置被称为一个“桶”,每个桶存储链表或树。
  3. 冲突处理:当两个键的哈希值相同时,它们会被存储在同一个桶中,形成一个链表(或树)。这种情况称为哈希冲突。
  4. 再哈希:当HashMap中的元素数量超过容量的负载因子(默认 0.75)时,HashMap会进行再哈希,将所有元素重新分配到一个更大的数组中。

注意事项

初始容量和负载因子:可以通过构造函数设置HashMap的初始容量和负载因子,以优化性能。初始容量越大,减少再哈希的次数;负载因子越小,减少冲突的概率,但会增加空间开销。

哈希函数的质量:哈希函数的质量直接影响HashMap的性能。理想的哈希函数应尽可能均匀地分布键。

线程安全性

如前所述,HashMap不是线程安全的。如果需要线程安全的映射,可以使用Collections.synchronizedMap来包装HashMap,或者使用ConcurrentHashMap,后者在高并发环境下性能更好。

Map<String, Integer> synchronizedMap = Collections.synchronizedMap(newHashMap<>());

或者使用ConcurrentHashMap:

Map<String, Integer> concurrentMap = newConcurrentHashMap<>();

21. HashMap怎么计算hashCode的?⭐⭐

HashMap使用键的hashCode()方法来生成哈希值,并对其进行一些处理,以提高哈希表的性能和均匀分布。

调用键的hashCode()方法

首先,HashMap调用键对象的hashCode()方法来获取一个整数哈希码。这个哈希码是由键对象的类定义的,通常是通过某种算法基于对象的内部状态计算出来的。

Plain Text int hashCode= key.hashCode();

扰动函数 (Perturbation Function)

为了减少哈希冲突并使哈希码更加均匀地分布,HashMap对原始哈希码进行了一些额外的处理。这种处理被称为扰动函数。Java 8 及以后的HashMap实现使用以下算法来计算最终的哈希值:

Plain Text static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

这个算法的步骤如下:

  1. 获取键的哈希码:h = key.hashCode()
  2. 右移 16 位:h >>> 16
  3. 异或运算:h ^ (h >>> 16)

这种方法通过将高位和低位的哈希码混合在一起,减少了哈希冲突的概率,从而使得哈希码更加均匀地分布在哈希表的桶中。

计算数组索引

计算出扰动后的哈希值后,HashMap使用这个值来确定键值对在哈希表中的位置。通常,HashMap使用哈希值对数组的长度取模(取余数)来计算索引:

Plain Text int index = (n - 1) & hash;

其中,n是哈希表数组的长度。n通常是 2 的幂,这样(n - 1)就是一个全 1 的二进制数,这使得按位与操作&可以有效地替代取模操作%,从而提高性能。

demo

假设我们有一个键对象,其hashCode()返回值为 123456。那么,计算哈希值的过程如下:

  1. 调用hashCode()方法:int hashCode = 123456;
  2. 扰动函数计算:

○ h = 123456

○ h >>> 16 = 123456 >>> 16 = 1(右移 16 位)

○ hash = h ^ (h >>> 16) = 123456 ^ 1 = 123457

  1. 计算数组索引(假设数组长度n为 16,即n - 1为 15):

○ index = (15) & 123457 = 15 & 123457 = 1

最终,键值对将存储在哈希表数组的索引 1 位置。

22. 抽象类能使用final修饰吗?⭐⭐

不能。

在Java中,抽象类不能使用final修饰。原因是final修饰符用于表示类不能被继承,而抽象类的主要用途就是被继承以提供基础实现或定义抽象方法供子类实现。这两个概念是互相矛盾的,因此不能同时使用。

abstract修饰符:用于定义一个抽象类,表示这个类不能被实例化,必须由子类继承并实现其抽象方法。

final修饰符:用于定义一个最终类,表示这个类不能被继承。

Java public final abstract class MyAbstractClass { // 编译错误:非法的修饰符组合:'final' 和 'abstract' }

编译器会报错,因为final和abstract是互斥的修饰符。

正确用法

如果你想定义一个抽象类,只需要使用abstract关键字:

Java public abstract class MyAbstractClass { public abstract void myAbstractMethod(); }

如果你想定义一个不能被继承的类,只需要使用final关键字:

Java public final class MyFinalClass { public void myMethod() { // 方法实现 } }

其他修饰符的组合

abstract和protected/public:可以组合使用,表示这个类是抽象的,并且可以被其他包中的类继承(如果是public)或在同一个包或子类中继承(如果是protected)。

abstract和default/private:不能组合使用,因为抽象类需要被继承,而private修饰符会阻止类被继承,default只能用于接口中的方法。

23. 成员变量与局部变量的区别⭐⭐⭐⭐⭐

定义位置

成员变量:定义在类中,但在方法、构造器或代码块之外。可以是实例变量或类变量(使用static修饰)。

public class MyClass {
    // 成员变量
    private int instanceVar;
    private static int classVar;
}

局部变量:定义在方法、构造器或代码块内部。只能在其定义的块内使用。

public class MyClass {
    public void myMethod() {
        // 局部变量
        int localVar = 10;
    }
}

生命周期

成员变量:实例变量的生命周期与对象的生命周期一致,对象被创建时分配内存,对象被垃圾回收时释放内存。类变量的生命周期与类的生命周期一致,类被加载时分配内存,类被卸载时释放内存。

局部变量:生命周期仅限于其所在的方法、构造器或代码块的执行期间。方法、构造器或代码块执行结束后,局部变量会被销毁。

默认值

成员变量:会被自动初始化为默认值(例如,数值类型为0,布尔类型为false,引用类型为null)。

public class MyClass {
    private int instanceVar; // 默认值为 0
    private boolean flag;    // 默认值为 false
}

局部变量:不会被自动初始化,必须显式初始化后才能使用,否则编译器会报错。

public class MyClass {
    public void myMethod() {
        int localVar; // 未初始化
        // System.out.println(localVar); // 编译错误:变量 localVar 可能尚未初始化
    }
}

访问修饰符

成员变量:可以使用访问修饰符(private、protected、public、默认)来控制其访问权限。

public class MyClass {
    private int instanceVar; // 私有访问
    public static int classVar; // 公共访问
}

局部变量:不能使用访问修饰符,作用域仅限于其定义的块内。

public class MyClass {
    public void myMethod() {
        int localVar = 10; // 不能使用访问修饰符
    }
}

存储位置

成员变量:实例变量存储在堆内存中。类变量存储在方法区中。

局部变量:存储在栈内存中。

修饰符

成员变量:可以使用static、final、transient、volatile等修饰符。

public class MyClass {
    private static final int CONSTANT = 100;
    private transient int transientVar;
    private volatile int volatileVar;
}

局部变量:可以使用final修饰,但不能使用static、transient、volatile。

public class MyClass { 
    public void myMethod() {
        final int localVar = 10; // 可以使用 final
    }
}

24. 静态变量和实例变量的区别⭐⭐⭐⭐⭐

  1. 归属不同

    • 静态变量 → 属于类本身(类加载时即存在)
    • 实例变量 → 属于具体对象(对象创建时才分配)
  2. 内存与共享性

    • 静态变量 → 全局唯一副本,所有对象共享(修改一处,全局生效)
    • 实例变量 → 每new一个对象就创建独立副本(修改互不影响)
      示例:静态变量如全校学生共享的校名;实例变量如每个学生独有的学号。
  3. 访问方式

    • 静态变量 → 直接通过类名访问(ClassName.var
    • 实例变量 → 必须通过对象访问(obj.var

25. final、finally、finalize区别⭐⭐

final:用于声明常量、不可重写的方法和不可继承的类。

finally:用于异常处理,确保某些代码总是会执行。

finalize:用于对象被垃圾回收之前的清理操作,但由于不确定性,不推荐依赖。

final

final是一个关键字,可以用于类、方法和变量,表示不可改变的特性。

final变量:一旦赋值后,不能再改变。必须在声明时或构造函数中初始化。

public class MyClass {
    public final int CONSTANT = 10;
    
    public MyClass() {
        // CONSTANT = 20; // 编译错误,不能重新赋值
    }
}

final方法:不能被子类重写。

public class ParentClass {
    public final void display() {
        System.out.println("This is a final method.");
    }
}

public class ChildClass extends ParentClass {
    // public void display() { // 编译错误,不能重写
    //     System.out.println("Cannot override final method.");
    // }
}

final类:不能被继承。

public final class FinalClass {
    // 类内容
}

// public class SubClass extends FinalClass { // 编译错误,不能继承
// }

finally

finally是一个用于异常处理的关键字,表示一个代码块,它总是会被执行,无论是否发生异常。通常用于释放资源等清理工作。

finally代码块:总是会执行,即使在try块中有return语句。

public class MyClass {
    public static void main(String[] args) {
        try {
            System.out.println("In try block");
            // return; // 即使有 return,finally 也会执行
        } catch (Exception e) {
            System.out.println("In catch block");
        } finally {
            System.out.println("In finally block");
        }
    }
}

finalize

finalize是一个方法,用于对象被垃圾回收器回收之前的清理操作。在Object类中定义,子类可以重写它来执行特定的清理操作。

finalize方法:在垃圾回收器确定没有对该对象的更多引用时调用。由于垃圾回收机制的不确定性,不推荐依赖finalize方法进行重要的清理工作。

public class MyClass {
    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("Finalize method called");
            // 清理代码
        } finally {
            super.finalize();
        }
    }
}

26. HashMap的主要参数都有哪些?⭐

初始容量(Initial Capacity)

初始容量是HashMap在创建时分配的桶(bucket)数组的大小。默认初始容量是 16。可以在创建HashMap时通过构造函数指定初始容量。

Plain Text HashMap\<K, V> map = newHashMap<>(initialCapacity);

负载因子(Load Factor)

负载因子是一个衡量HashMap何时需要调整大小(即扩容)的参数。默认负载因子是 0.75,这意味着当HashMap中的条目数达到当前容量的 75% 时,HashMap会进行扩容。负载因子越低,哈希表中的空闲空间越多,冲突越少,但空间利用率也越低。

Plain Text HashMap\<K, V> map = newHashMap<>(initialCapacity, loadFactor);

阈值(Threshold)

阈值是HashMap需要扩容的临界点,计算方式为初始容量 * 负载因子。当实际存储的键值对数量超过这个阈值时,HashMap会进行扩容。

桶(Bucket)

HashMap内部使用一个数组来存储链表或树(在 Java 8 及之后的版本中,当链表长度超过一定阈值时,会转化为树)。每个数组元素称为一个桶(bucket)。哈希值经过计算后决定了键值对存储在哪个桶中。

哈希函数(Hash Function)

HashMap使用哈希函数将键的哈希码转换为数组索引。Java 的HashMap使用了扰动函数(perturbation function)来减少哈希冲突:

Plain Text static final int hash(Object key) {   
int h;  
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
}

链表和树(Linked List and Tree)

在桶中的键值对存储方式上,HashMap使用链表来处理哈希冲突。在 Java 8 及之后的版本中,当链表的长度超过阈值(默认是 8)时,链表会转换为红黑树,以提高查找效率。

红黑树转换阈值(Treeify Threshold)

这是一个阈值,当单个桶中的链表长度超过这个值时,链表会转换为红黑树。默认值是 8。

最小树化容量(Minimum Treeify Capacity)

这是一个阈值,当HashMap的容量小于这个值时,即使链表长度超过Treeify Threshold,也不会将链表转换为红黑树,而是会先进行扩容。默认值是 64。

扩容因子(Resize Factor)

当HashMap的大小超过阈值时,容量会加倍。即新的容量是旧容量的两倍。

迭代器(Iterators)

HashMap提供了键、值和条目的迭代器,用于遍历HashMap中的元素。迭代器是快速失败的(fail-fast),即在迭代过程中,如果HashMap结构被修改(除了通过迭代器自身的remove方法),迭代器会抛出ConcurrentModificationException。

版本(ModCount)

HashMap维护了一个内部版本号modCount,用于跟踪HashMap的结构修改次数。这在迭代器中用于检测并发修改。

这些参数和属性共同决定了HashMap的性能和行为。理解这些参数可以帮助开发者更好地使用HashMap,并在需要时进行适当的调整以满足特定的性能需求。

27. 解决hash碰撞的方法?⭐⭐⭐⭐⭐

链地址法(Chaining)

链地址法是最常见的解决哈希碰撞的方法之一。在这种方法中,每个桶(bucket)包含一个链表(或树结构,Java 8 及以上版本)。当发生哈希碰撞时,新的键值对被添加到相应桶的链表中。

优点:

简单易实现。

动态调整链表长度,不需要提前知道元素数量。

缺点:

当链表长度增加时,查找效率下降。

需要额外的存储空间来存储指针。

class HashMapNode<K, V> {
    K key;
    V value;
    HashMapNode<K, V> next;
    HashMapNode(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

开放地址法(Open Addressing)

开放地址法不使用链表,而是在哈希表本身寻找空闲位置来存储碰撞的元素。常见的开放地址法有以下几种:

线性探测(Linear Probing)

当发生哈希碰撞时,线性探测法在哈希表中向后依次查找下一个空闲位置。

优点:实现简单,不需要额外的存储空间。

缺点:当哈希表接近满时,查找效率急剧下降(称为“主群集”问题)。

int hash = key.hashCode() % table.length;
while (table[hash] != null) {
    hash = (hash + 1) % table.length;
}
table[hash] = new Entry(key, value);

二次探测(Quadratic Probing)

二次探测法在发生哈希碰撞时,按照平方序列查找空闲位置(如 1, 4, 9, 16, ...)。

优点:减少主群集问题。

缺点:实现较复杂,可能会导致二次群集问题。

int hash = key.hashCode() % table.length;
int i = 1;
while (table[hash] != null) {
    hash = (hash + i * i) % table.length;
    i++;
}
table[hash] = new Entry(key, value);

双重散列(Double Hashing)

双重散列法使用两个不同的哈希函数。当第一个哈希函数发生碰撞时,使用第二个哈希函数计算新的索引。

优点:减少群集问题。较好的查找性能。

缺点:实现复杂。需要设计两个有效的哈希函数。

int hash1 = key.hashCode() % table.length;
int hash2 = 1 + (key.hashCode() % (table.length - 1));
while (table[hash1] != null) {
    hash1 = (hash1 + hash2) % table.length;
}
table[hash1] = new Entry(key, value);

再哈希法(Rehashing)

再哈希法在发生碰撞时,使用不同的哈希函数重新计算哈希值,直到找到空闲位置。

优点:减少群集问题。

缺点:实现复杂。需要设计多个有效的哈希函数。

分离链接法

在 Java 8 及以上版本中,当链表长度超过一定阈值(默认是 8)时,链表会转换为红黑树,以提高查找效率。

优点:在高冲突情况下性能较好,动态调整链表和树的长度。

缺点:实现复杂,需要额外的存储空间。

其他方法

Cuckoo Hashing:使用两个哈希表和两个哈希函数,如果插入时发生冲突,将原来的元素“踢出”并重新插入到另一个哈希表中。

Hopscotch Hashing:类似于线性探测,但在插入时会调整元素的位置,使得查找路径更短。

链地址法是最常见的解决哈希碰撞的方法,适用于大多数情况。开放地址法在空间利用率上有优势,但在高负载情况下性能可能下降。再哈希法和其他高级方法适用于特定的高性能需求场景。

28. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?⭐⭐

HashMap的负载因子(load factor)初始值设为 0.75 是一个经过权衡的结果,主要考虑了性能和内存使用之间的平衡。

性能与内存使用的平衡

查找性能:在HashMap中,查找操作的时间复杂度接近 (O(1))。然而,当哈希表中的元素过多时,链地址法中的链表会变长,查找时间会增加。负载因子为 0.75 意味着在表达到 75% 满时进行扩容,这样可以保持链表的长度较短,从而保证查找操作的高效性。

内存使用:如果负载因子设置得太低(例如 0.5),HashMap会更频繁地扩容,需要更多的内存来存储未使用的桶。负载因子为 0.75 是一个较为合理的设置,可以在保证查找性能的同时,节约内存。

扩容频率

较高的负载因子(如 1.0)会减少扩容的频率,但会导致较长的链表或更多的哈希碰撞,从而影响查找性能。较低的负载因子(如 0.5)会增加扩容的频率,虽然可以减少碰撞,但会导致更多的空间浪费。

0.75 是一个折中的选择,它既能保证较少的哈希碰撞,又不会频繁地进行扩容,从而在性能和内存使用之间取得平衡。

实际应用中的经验

在实际应用中,0.75 被证明是一个有效的默认值。它在大多数情况下提供了良好的性能和较为合理的内存使用。尽管特定应用可能有不同的需求,但对于通用场景,这个默认值是经过大量实践验证的。

负载因子的灵活性

虽然 0.75 是默认值,开发者在创建HashMap时可以根据具体需求指定不同的负载因子。例如:

Map<Integer, String> map = newHashMap<>(initialCapacity, 0.5f);

在上述代码中,HashMap的负载因子被设置为 0.5,这可能适用于需要更高查找性能但内存使用不是主要考虑因素的场景。

HashMap默认负载因子为 0.75 是一个经过深思熟虑的选择,旨在平衡查找性能和内存使用。它在大多数情况下提供了良好的性能表现,同时避免了频繁扩容和过多的内存浪费。开发者可以根据具体需求调整负载因子,以适应不同的应用场景。

29. 什么是Java跨平台性?⭐⭐⭐⭐⭐

Java的跨平台性(Platform Independence)是指Java程序可以在不同的操作系统和硬件平台上运行,而无需修改代码。这一特性主要源于Java虚拟机(JVM)和Java编译器的设计。

Java跨平台性的实现机制

Java跨平台性的核心在于“编译一次,到处运行”(Write Once, Run Anywhere,WORA)的理念。具体来说,这一特性通过以下机制实现:

Java编译器(javac):

Java源代码(.java文件)首先被编译器编译成中间表示形式的字节码(.class文件),这些字节码是与平台无关的中间代码。

Java虚拟机(JVM):

不同的平台(如Windows、Linux、macOS等)都有相应的JVM实现。JVM负责将字节码解释或即时编译(JIT)成特定平台的机器代码,并执行这些代码。由于每个平台都有其对应的JVM,Java字节码可以在任何安装了JVM的系统上运行。

工作流程

编写代码:开发者在任何平台上编写Java源代码。

编译代码:使用Java编译器javac将源代码编译成字节码。

Java javac MyProgram.java

运行代码:使用JVM执行生成的字节码。

Java java MyProgram

优势

便捷性:开发者只需编写和编译一次代码,就可以在多个平台上运行,减少了开发和维护的成本。

广泛的适用性:Java程序可以在任何支持JVM的平台上运行,包括桌面操作系统、服务器和嵌入式设备等。

一致性:由于字节码和JVM的标准化,Java程序在不同平台上的行为是一致的。

demo

假设有一个简单的Java程序HelloWorld.java:

Java public class HelloWorld {   
    public static void main(String[] args) {     
        System.out.println("Hello, World!");   
    } 
}

编译:这将生成一个HelloWorld.class文件,包含平台无关的字节码。

Java javac HelloWorld.java

运行:在任何安装了JVM的系统上运行:

Java java HelloWorld

无论是在Windows、Linux还是macOS上,输出都是:

Java Hello, World!

30. Java中的数据类型⭐⭐⭐⭐⭐

HashMap的负载因子(load factor)初始值设为 0.75 是一个经过权衡的结果,主要考虑了性能和内存使用之间的平衡。

性能与内存使用的平衡

查找性能:在HashMap中,查找操作的时间复杂度接近 (O(1))。然而,当哈希表中的元素过多时,链地址法中的链表会变长,查找时间会增加。负载因子为 0.75 意味着在表达到 75% 满时进行扩容,这样可以保持链表的长度较短,从而保证查找操作的高效性。

内存使用:如果负载因子设置得太低(例如 0.5),HashMap会更频繁地扩容,需要更多的内存来存储未使用的桶。负载因子为 0.75 是一个较为合理的设置,可以在保证查找性能的同时,节约内存。

扩容频率

较高的负载因子(如 1.0)会减少扩容的频率,但会导致较长的链表或更多的哈希碰撞,从而影响查找性能。较低的负载因子(如 0.5)会增加扩容的频率,虽然可以减少碰撞,但会导致更多的空间浪费。

0.75 是一个折中的选择,它既能保证较少的哈希碰撞,又不会频繁地进行扩容,从而在性能和内存使用之间取得平衡。

实际应用中的经验

在实际应用中,0.75 被证明是一个有效的默认值。它在大多数情况下提供了良好的性能和较为合理的内存使用。尽管特定应用可能有不同的需求,但对于通用场景,这个默认值是经过大量实践验证的。

负载因子的灵活性

虽然 0.75 是默认值,开发者在创建HashMap时可以根据具体需求指定不同的负载因子。例如:

Map<Integer, String> map = newHashMap<>(initialCapacity, 0.5f);

在上述代码中,HashMap的负载因子被设置为 0.5,这可能适用于需要更高查找性能但内存使用不是主要考虑因素的场景。

HashMap默认负载因子为 0.75 是一个经过深思熟虑的选择,旨在平衡查找性能和内存使用。它在大多数情况下提供了良好的性能表现,同时避免了频繁扩容和过多的内存浪费。开发者可以根据具体需求调整负载因子,以适应不同的应用场景。

31. 重新调整HashMap大小存在什么问题吗?⭐⭐

性能影响

时间复杂度:扩容是一个相对昂贵的操作,因为它需要重新计算所有现有键值对的哈希值,并将它们重新分配到新的桶数组中。这个过程的时间复杂度为 (O(n)),其中 (n) 是哈希表中元素的数量。因此,在扩容期间,可能会导致性能的短暂下降,尤其是在插入大量数据时。

阻塞操作:在单线程环境中,扩容会阻塞其他操作(如查找、插入、删除),直到扩容完成。在多线程环境中,如果没有适当的同步机制,扩容可能会导致数据不一致或其他并发问题。

内存使用

临时内存消耗:扩容期间,HashMap需要分配一个新的桶数组,同时保留旧的桶数组,直到重新哈希完成。这会导致临时的内存消耗增加。如果哈希表非常大,可能会导致内存不足的问题。

内存碎片:频繁的扩容和缩容可能导致内存碎片化,降低内存利用效率。

并发问题

线程安全:默认的HashMap不是线程安全的。在多线程环境中,如果一个线程在进行扩容操作,而另一个线程在进行插入或删除操作,可能会导致数据不一致或程序崩溃。为了解决这个问题,可以使用ConcurrentHashMap或在外部进行同步。

扩容期间的数据一致性:在扩容过程中,如果有其他线程在进行读写操作,可能会导致数据不一致。因此,在多线程环境中,必须确保扩容操作是原子的,或者使用并发安全的数据结构。

重新哈希的成本

哈希函数的复杂性:重新哈希所有键值对需要调用哈希函数。如果哈希函数较为复杂,重新哈希的成本也会增加。

哈希冲突的处理:在扩容过程中,哈希冲突的处理(如链地址法中的链表或红黑树)也会增加额外的开销。

应用层面的影响

实时性要求:在某些实时性要求较高的应用中,扩容操作可能导致短暂的性能下降,影响系统的响应时间。

数据一致性要求:在某些应用中,数据的一致性要求较高,扩容过程中可能会导致短暂的数据不一致,需要额外的机制来保证一致性。

解决方案和优化

  1. 预估初始容量:如果可以预估数据量,尽量在创建HashMap时设置合适的初始容量,减少扩容次数。
  2. 使用并发数据结构:在多线程环境中,使用ConcurrentHashMap代替HashMap,它采用了分段锁机制,减少了扩容带来的并发问题。
  3. 动态调整负载因子:根据应用需求,动态调整负载因子以适应数据量的变化。

32. HashMap,扩容过程,怎么解决哈希冲突?⭐⭐

HashMap 扩容过程

Hashmap 的扩容(rehashing)主要发生在以下两种情况下:

  1. 当添加元素时,如果当前数组为空,会进行初始化:默认情况下,会创建一个长度为 16 的数组,并且加载因子(load factor)默认为 0.75。
  2. 当数组中的元素数量大于或等于数组长度与加载因子的乘积时:例如,当数组长度为 16,加载因子为 0.75,并且元素数量达到 12 时(16 * 0.75 = 12),会触发扩容。扩容时,数组长度会翻倍(通常是 2 的幂),并重新哈希所有元素到新的数组中。

在扩容过程中,hashmap 会重新计算每个元素的哈希值,并根据新的数组长度重新定位其索引位置。由于数组长度翻倍,哈希值的位运算结果可能会改变,导致元素在新数组中的位置与旧数组不同。

哈希冲突解决

哈希冲突(hash collision)是指不同的键计算出相同的哈希值,从而在哈希表中映射到同一个位置。HashMap 通过以下策略来解决哈希冲突:

  1. 链表法(链表或红黑树):在 HashMap 中,每个位置(索引)可以存储一个链表(或红黑树,当链表长度超过一定阈值时)。当发生哈希冲突时,新的元素会被添加到对应的链表中。在 Java 8 及之后的版本中,当链表长度达到 8 且数组长度大于 64 时,链表会转换为红黑树以优化性能。
  2. 哈希函数:为了降低哈希冲突的概率,HashMap 使用了一个精心设计的哈希函数来计算键的哈希值。这个哈希函数考虑了键对象的哈希码(hashCode)以及键在数组中的索引位置,通过一些位运算得到最终的哈希值。这样可以确保哈希值的分布尽可能均匀,减少冲突的可能性。
  3. 初始容量和加载因子:初始容量和加载因子也会影响哈希冲突的概率。较大的初始容量和较小的加载因子可以降低哈希冲突的概率,但也会增加空间开销。因此,在选择这些参数时需要根据具体需求进行权衡。

总的来说,HashMap 通过链表法(或红黑树)和精心设计的哈希函数来解决哈希冲突,并通过扩容和重新哈希来保持哈希表的性能和效率。

33. private、public、protected及默认访问修饰符的区别⭐

在Java中,访问修饰符(access modifiers)用于控制类、方法和变量的访问级别。主要有四种访问修饰符:private、public、protected和 默认(不写)。

它们的区别如下:

private

访问范围:仅在同一个类内可访问。

使用场景:用于隐藏类的实现细节,保护类的成员变量和方法不被外部类访问和修改。

class Example {
    private int privateVar;

    private void privateMethod() {
        // 仅在Example类内部可访问
    }
}

public

访问范围:在任何地方都可以访问。

使用场景:用于类、方法或变量需要被其他类访问的情况。

public class Example {
    public int publicVar;

    public void publicMethod() {
        // 在任何地方都可以访问
    }
}

protected

访问范围:在同一个包内,以及在不同包中的子类中可访问。

使用场景:用于希望在同一个包内或子类中访问,但不希望在包外的非子类中访问的情况。

class Example {
    protected int protectedVar;

    protected void protectedMethod() {
        // 在同一个包内或不同包中的子类中可访问
    }
}

默认(不写)

访问范围:仅在同一个包内可访问。

使用场景:用于包级访问控制,不希望类、方法或变量被包外的类访问。

class Example {
    int defaultVar; // 默认访问修饰符

    void defaultMethod() {
        // 仅在同一个包内可访问
    }
}

访问修饰符的总结

修饰符同一个类同一个包子类(不同包)其他包
private
默认(不写)
protected
public

代码 demo

package package1;

public class PublicClass {
    private int privateVar;
    int defaultVar;
    protected int protectedVar;
    public int publicVar;

    private void privateMethod() {}
    void defaultMethod() {}
    protected void protectedMethod() {}
    public void publicMethod() {}
}

package package2;

import package1.PublicClass;

public class SubClass extends PublicClass {
    public void accessMethods() {
        // privateMethod(); // 错误,无法访问
        // defaultMethod(); // 错误,无法访问
        protectedMethod(); // 正确,可以访问
        publicMethod(); // 正确,可以访问
    }
}

package package1;

public class SamePackageClass {
    public void accessMethods() {
        PublicClass obj = new PublicClass();
        // obj.privateMethod(); // 错误,无法访问
        obj.defaultMethod(); // 正确,可以访问
        obj.protectedMethod(); // 正确,可以访问
        obj.publicMethod(); // 正确,可以访问
    }
}

34. 为什么hashmap多线程会进入死循环?⭐

HashMap在多线程环境中可能会进入死循环,主要是由于其非线程安全的设计导致的。

并发修改导致的链表环

在HashMap中,当发生哈希冲突时,使用链地址法(链表)来存储冲突的键值对。如果多个线程同时对HashMap进行修改(例如插入或删除操作),可能会导致链表结构被破坏,形成环形链表。这种情况下,当遍历链表时,会陷入死循环。

原因分析

当两个或多个线程同时修改HashMap,例如在同一个桶中插入元素,可能会导致链表的指针被错误地更新。例如,一个线程正在将一个新的节点插入链表中,而另一个线程正在重新排列链表的顺序。这种竞争条件可能导致链表中出现环形结构。

示例代码

import java.util.HashMap;
import java.util.Map;
public class HashMapInfiniteLoop {
    public static void main(String[] args) {
        final Map<Integer, Integer> map = new HashMap<>();
        // 创建两个线程同时对 HashMap 进行插入操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                map.put(i, i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 10000; i < 20000; i++) {
                map.put(i, i);
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 遍历 HashMap,可能会陷入死循环
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

在上述代码中,两个线程同时对HashMap进行插入操作,可能会导致链表结构被破坏,形成环形链表,从而在遍历时陷入死循环。

扩容导致的并发问题

HashMap在容量达到一定阈值时会进行扩容(rehash),即重新分配桶数组,并重新哈希所有键值对。如果在扩容过程中,有其他线程同时进行插入操作,可能会导致重新哈希过程中的数据不一致,进而引发死循环。

原因分析

扩容过程中,HashMap会创建一个新的、更大的桶数组,并将所有旧的键值对重新哈希并放入新的桶中。如果在这个过程中有其他线程插入新的键值对,可能会导致旧桶和新桶的数据结构不一致,进而引起死循环。

示例代码

import java.util.HashMap;
import java.util.Map;
public class HashMapResizeInfiniteLoop {
    public static void main(String[] args) {
        final Map<Integer, Integer> map = new HashMap<>(2);
        // 创建两个线程同时对 HashMap 进行插入操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                map.put(i, i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 10000; i < 20000; i++) {
                map.put(i, i);
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 遍历 HashMap,可能会陷入死循环
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

在上述代码中,HashMap初始容量设置为 2,两个线程同时插入大量元素,可能会导致扩容过程中数据不一致,从而引发死循环。

解决方案

使用线程安全的数据结构

在多线程环境中,使用ConcurrentHashMap代替HashMap。ConcurrentHashMap通过分段锁机制来保证线程安全,并发性能更好。

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                map.put(i, i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 10000; i < 20000; i++) {
                map.put(i, i);
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

外部同步

如果必须使用HashMap,可以在外部进行同步,确保同时只有一个线程对HashMap进行修改。

import java.util.HashMap;
import java.util.Map;
public class SynchronizedHashMapExample {
    public static void main(String[] args) {
        final Map<Integer, Integer> map = new HashMap<>();
        Thread t1 = new Thread(() -> {
            synchronized (map) {
                for (int i = 0; i < 10000; i++) {
                    map.put(i, i);
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (map) {
                for (int i = 10000; i < 20000; i++) {
                    map.put(i, i);
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (map) {
            for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
                System.out.println(entry.getKey() + " : " + entry.getValue());
            }
        }
    }
}

通过使用ConcurrentHashMap或外部同步,可以避免HashMap在多线程环境中出现死循环的问题。

36. final关键字的作用⭐⭐⭐⭐⭐

在Java中,final关键字可以用于类、方法和变量,分别有不同的作用。

final变量

当一个变量被声明为final时,它的值在初始化之后就不能被改变。final变量必须在声明时或通过构造函数进行初始化。

public class Example {
    final int finalVar = 10; // 声明时初始化

    final int anotherFinalVar;

    public Example(int value) {
        anotherFinalVar = value; // 通过构造函数初始化
    }

    public void changeValue() {
        // finalVar = 20; // 编译错误,final变量的值不能被改变
    }
}

final方法

当一个方法被声明为final时,它不能被子类重写(override)。这可以用来防止子类改变父类中关键的方法实现。

public class ParentClass {
    public final void finalMethod() {
        System.out.println("This is a final method.");
    }
}

public class ChildClass extends ParentClass {
    // @Override
    // public void finalMethod() { // 编译错误,无法重写final方法
    //     System.out.println("Trying to override final method.");
    // }
}

final类

当一个类被声明为final时,它不能被继承。这对于创建不可变类(immutable class)或确保类的实现不被改变是非常有用的。

public final class FinalClass {
    // 类的内容
}

// public class SubClass extends FinalClass { // 编译错误,无法继承final类
//     // 子类内容
// }

final和不可变对象

final关键字在创建不可变对象时非常有用。不可变对象的状态在创建之后不能被改变,这在多线程环境中尤其重要,因为它们是线程安全的。

public final class ImmutableClass {
    private final int value;

    public ImmutableClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

final和局部变量

final也可以用于局部变量,尤其是在匿名类或lambda表达式中使用时。被声明为final的局部变量在方法执行期间不能被修改。

public class Example {
    public void method() {
        final int localVar = 10;

        // localVar = 20; // 编译错误,无法修改final局部变量

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(localVar); // 可以在匿名类中访问final局部变量
            }
        };

        runnable.run();
    }
}

总结

final变量:值不能被改变。

final方法:不能被重写。

final类:不能被继承。

final局部变量:在方法执行期间不能被修改,尤其在匿名类和lambda表达式中有用。

37. 为什么String, Interger这样的wrapper类适合作为键?⭐⭐

不可变性

String和Integer等包装类都是不可变的对象。一旦创建,这些对象的状态就不能被改变。不可变性是一个重要的特性,因为它保证了对象在其生命周期内的哈希码(hash code)不会改变。

如果一个对象在作为键的过程中其哈希码发生了改变,那么在哈希表中查找该键时将无法找到正确的位置,导致数据结构无法正常工作。不可变对象避免了这一问题。

合理的hashCode()实现

String和Integer类都提供了高质量的hashCode()方法,这些方法能够有效地分布哈希值,减少哈希冲突。具体来说:

String的hashCode()方法是基于字符串内容计算的,使用了一个高效的算法。

Integer的hashCode()方法直接返回其内部存储的整数值。

合理的equals()实现

String和Integer类都提供了正确且高效的equals()方法,这些方法能够准确地比较两个对象的内容是否相等。这对于哈希表等数据结构来说是至关重要的,因为在哈希表中查找键时需要依赖equals()方法来判断两个键是否相等。

内存效率

虽然包装类相对于原始类型有一些额外的内存开销,但这些类通常经过了优化,能够在大多数情况下提供足够的性能和内存效率。例如,Integer类使用了对象池来缓存常用的整数值(-128 到 127),从而减少了内存消耗和对象创建的开销。

38. jdk7的hashmap实现?⭐⭐⭐⭐

HashMap在 JDK 7 中的实现其实并不复杂,它主要依靠两个数据结构:数组和链表。

首先,HashMap内部有一个数组,这个数组用来存储所有的键值对。每个数组的元素其实是一个链表的头节点。也就是说,如果两个或多个键计算出来的哈希值相同,它们会被存储在同一个数组位置的链表中。

当我们往HashMap里放一个键值对时,HashMap会先根据键的hashCode计算出一个哈希值,然后用这个哈希值决定键值对应该放在数组的哪个位置。如果那个位置是空的,键值对就直接放进去;如果那个位置已经有其他键值对了(也就是发生了哈希冲突),HashMap会把新的键值对放到那个位置的链表上。

举个例子吧,假设我们有一个HashMap,我们要往里面放一个键值对("apple", 1)。HashMap会先计算"apple"的哈希值,然后用这个哈希值决定应该把它放到数组的哪个位置。假如计算出来的位置是 5,如果数组的第 5 个位置是空的,它就直接放进去;如果已经有其他键值对了,比如("banana", 2),它就会把("apple", 1)加到("banana", 2)的链表上。

取值的时候也类似。假设我们要取"apple"对应的值,HashMap会先计算"apple"的哈希值,然后找到数组的对应位置,再沿着链表找到"apple"对应的节点,最后返回它的值。

需要注意的是,HashMap不是线程安全的。如果多个线程同时修改HashMap,可能会导致一些奇怪的问题,比如死循环。所以在多线程环境下,建议使用ConcurrentHashMap。

总结一下,HashMap在 JDK 7 中主要是通过数组和链表来存储数据,使用哈希值来决定存储位置,并通过链表来解决哈希冲突。它的设计让我们在大多数情况下能够快速地存取数据,但在多线程环境下需要小心使用。

JDK 7 中的HashMap底层实现方式主要基于数组和链表。它通过哈希函数将键映射到数组中的索引位置,从而实现快速的查找和存储操作。

数据结构

HashMap主要由以下几部分组成:

数组(table):存储HashMap的核心数据结构。每个数组元素是一个链表的头节点。

链表(Entry):处理哈希冲突的结构。当多个键的哈希值映射到同一个数组索引时,这些键值对会被存储在该索引位置的链表中。

Entry 类

在 JDK 7 中,HashMap使用一个内部类Entry来表示键值对。Entry类的定义如下:

static class Entry<K, V> implements Map.Entry<K, V> {
    final K key;
    V value;
    Entry<K, V> next;
    final int hash;
    Entry(int h, K k, V v, Entry<K, V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry) o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    public final int hashCode() {
        return (key == null ? 0 : key.hashCode()) ^
               (value == null ? 0 : value.hashCode());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
}

存储过程

当向HashMap中存储一个键值对时,主要步骤如下:

  1. 计算哈希值:通过键的hashCode()方法计算哈希值,并进一步处理以减少冲突。
  2. 确定数组索引:通过哈希值计算数组索引位置。
  3. 插入节点:如果数组索引位置为空,则直接插入。如果不为空,则需要处理哈希冲突。

处理哈希冲突

在 JDK 7 中,HashMap通过链表法处理哈希冲突。当多个键的哈希值映射到同一个数组索引时,这些键值对会被存储在该索引位置的链表中。插入时,新节点会被插入到链表的头部。

代码示例

以下是put方法的简化版本,展示了HashMap的存储过程:

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K, V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key.hashCode()) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K, V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

取值过程

取值时,通过键计算哈希值和数组索引,然后在链表中查找对应的键值对。

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K, V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

39. this与super的区别⭐⭐⭐⭐⭐

this关键字

this关键字用于引用当前对象的实例。

1、引用当前对象的实例变量: 当局部变量和实例变量同名时,this关键字可以用于区分它们。

public class Example {
    private int value;

    public Example(int value) {
        // this.value指的是实例变量,value指的是参数
        this.value = value; 
    }
}

2、调用当前对象的方法: 可以使用this关键字来调用当前对象的另一个方法。

public class Example {
    public void method1() {
        System.out.println("Method 1");
    }

    public void method2() {
        this.method1(); // 调用当前对象的method1
    }
}

3、 调用当前对象的构造函数: 在一个构造函数中,可以使用this关键字调用同一个类中的另一个构造函数。必须是构造函数的第一行。

public class Example {
    private int value;
    private String text;

    public Example(int value) {
        this(value, "default text"); // 调用另一个构造函数
    }

    public Example(int value, String text) {
        this.value = value;
        this.text = text;
    }
}

super关键字

super关键字用于引用父类(超类)的成员。它有以下几种主要用途:

1、 调用父类的构造函数: 在子类的构造函数中,可以使用super关键字调用父类的构造函数。必须是构造函数的第一行。

public class Parent {
    public Parent() {
        System.out.println("Parent constructor");
    }
}

public class Child extends Parent {
    public Child() {
        super(); // 调用父类的构造函数
        System.out.println("Child constructor");
    }
}

2、 引用父类的实例变量: 当子类和父类有同名的实例变量时,可以使用super关键字访问父类的实例变量。

public class Parent {
    protected int value = 10;
}

public class Child extends Parent {
    private int value = 20;

    public void printValues() {
        System.out.println(super.value); // 访问父类的value
        System.out.println(this.value);  // 访问子类的value
    }
}

3、 调用父类的方法: 可以使用super关键字调用父类的方法。

public class Parent {
    public void method() {
        System.out.println("Parent method");
    }
}

public class Child extends Parent {
    @Override
    public void method() {
        super.method(); // 调用父类的方法
        System.out.println("Child method");
    }
}

this主要用于当前对象的引用,而super主要用于父类的引用。

40. == 和 equals 的区别⭐⭐⭐⭐⭐

在Java中,==和equals()方法用于比较对象,但它们的用途和行为是不同的。

== 操作符

\== 是一个比较操作符,用于比较两个操作数的内存地址(引用)是否相同。它可以用于比较基本数据类型和对象引用。

用于基本数据类型

对于基本数据类型(如int、char、boolean等),==比较的是它们的值。

int a = 5;
int b = 5;
System.out.println(a == b); // 输出 true,因为值相同

用于对象引用

对于对象引用,==比较的是两个对象在内存中的地址是否相同,即它们是否引用同一个对象。

String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false,因为它们是不同的对象

String str3 = str1;
System.out.println(str1 == str3); // 输出 true,因为它们引用同一个对象

equals()方法

equals()方法是Object类中的一个方法,用于比较两个对象的内容是否相同。默认情况下,Object类的equals()方法与==操作符的行为相同,即比较对象的引用。但许多类(如String、Integer等)重写了equals()方法,用于比较对象的内容。

默认实现

默认的equals()方法在Object类中定义,比较的是对象的引用。

public boolean equals(Object obj) {
    return (this == obj);
}

重写的equals()方法

许多类重写了equals()方法,用于比较对象的内容。例如,String类重写了equals()方法,用于比较字符串的内容。

String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // 输出 true,因为字符串内容相同

自定义类中的equals()方法

在自定义类中,可以重写equals()方法来比较对象的内容。

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

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
}

Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
System.out.println(p1.equals(p2)); // 输出 true,因为内容相同

使用==操作符时,比较的是对象的引用是否相同,除非用于基本数据类型。

使用equals()方法时,比较的是对象的内容(如果该方法被重写的话)。

41. java8的hashmap实现?⭐⭐⭐⭐

在Java 8中,HashMap的实现进行了显著的优化,特别是在处理哈希冲突方面,引入了红黑树数据结构。这些改进旨在提高在高冲突情况下的性能。

数据结构

HashMap的底层结构仍然是基于数组和链表的组合,但在Java 8中,当链表长度超过一定阈值时,会将链表转换为红黑树,以提高操作效率。

存储过程

  1. 计算哈希值:首先,通过键的hashCode方法计算哈希值,然后对该哈希值进行扰动,以减少冲突。扰动的目的是为了使哈希值更加均匀地分布在数组中。
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 确定数组索引:通过哈希值与数组长度的减一值进行按位与运算,计算出数组的索引位置。
static int indexFor(int h, int length) {
    return h & (length - 1);
}
  1. 插入节点

如果数组索引位置为空,直接插入新的节点。

如果不为空,则需要处理哈希冲突。

处理哈希冲突

在Java 8中,处理哈希冲突的方法有了显著改进:

  1. 链表:如果冲突的节点数较少(链表长度小于等于8),则使用链表存储。链表的插入操作在链表尾部进行,以保持插入顺序。
  2. 红黑树:如果链表长度超过8,长度大于 64HashMap会将链表转换为红黑树。红黑树是一种自平衡的二叉搜索树,其查找、插入和删除操作的时间复杂度为O(log n),相比链表的O(n)更高效。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);

取值过程

在取值时,HashMap会先计算哈希值,然后找到对应的数组位置。如果该位置存储的是链表,则遍历链表查找;如果是红黑树,则在树中查找。

扩容

当HashMap中的元素数量超过一定阈值(通常是数组长度的0.75倍)时,会进行扩容。扩容时,HashMap会创建一个新的、更大的数组,并将旧数组中的所有元素重新哈希并放入新数组中。

void resize(int newCapacity) {
    Node<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    Node<K,V>[] newTable = (Node<K,V>[])new Node[newCapacity];
    // Rehashing elements to new table
    for (int j = 0; j < oldCapacity; ++j) {
        Node<K,V> e;
        if ((e = oldTable[j]) != null) {
            oldTable[j] = null;
            if (e.next == null)
                newTable[e.hash & (newCapacity - 1)] = e;
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTable, j, oldCapacity);
            else { // preserve order
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    if ((e.hash & oldCapacity) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
                if (loTail != null) {
                    loTail.next = null;
                    newTable[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTable[j + oldCapacity] = hiHead;
                }
            }
        }
    }
    table = newTable;
}

Java 8中的HashMap通过引入红黑树来优化哈希冲突的处理。当链表长度超过一定阈值时转换为红黑树,从而在极端情况下提高查找和插入的效率。这些改进使得HashMap在大多数情况下能够提供更稳定和高效的性能。

42. hashCode()与equals()的关系⭐⭐⭐⭐⭐

hashCode()方法

hashCode()方法返回一个整数,称为哈希码。哈希码用于在哈希表中快速查找对象。Object类中的默认实现返回对象的内存地址转换成的整数。

equals()方法

equals()方法用于比较两个对象的内容是否相同。Object类中的默认实现比较的是对象的引用。

hashCode()与equals()的合同

为了确保哈希表中的对象行为正确,Java规范定义了hashCode()与equals()方法之间的合同(契约):

一致性

1、 如果两个对象根据equals()方法比较是相等的,那么它们的hashCode()方法必须返回相同的整数。

2、 如果两个对象根据equals()方法比较是不相等的,那么它们的hashCode()方法不一定要返回不同的整数,但不同的对象返回不同的哈希码可以提高哈希表的性能。

稳定性

在程序执行期间,hashCode()方法返回的整数值应该是一致的,只要对象的状态没有改变。也就是说,同一个对象多次调用hashCode()方法应该返回相同的值。

实现hashCode()与equals()方法的 demo

假设有一个自定义类Person,我们需要重写equals()和hashCode()方法。

import java.util.Objects;

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

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }

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

equals()方法

首先检查是否与自身比较(this == obj),如果是,返回true。

然后检查传入的对象是否为空或类型是否匹配(obj == null || getClass() != obj.getClass()),如果不匹配,返回false。

最后,比较类的字段是否相等(age == person.age && Objects.equals(name, person.name))。

hashCode()方法:

使用Objects.hash()方法生成哈希码,该方法根据传入的字段生成一个哈希码。

重要注意项

重写equals()方法时,必须同时重写hashCode()方法,以确保两个相等的对象具有相同的哈希码。

如果只重写equals()方法而不重写hashCode()方法,可能会导致在哈希表中查找、插入和删除对象时出现问题。

43. jdk8的hashmap的put过程?⭐⭐

put方法的实现

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

put方法调用了putVal方法。这里的hash(key)是计算键的哈希值。

计算哈希值

hash方法用于计算键的哈希值并进行扰动处理,以减少冲突。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal方法的实现

putVal方法是HashMap中实际执行插入操作的核心方法。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == 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))))
            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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                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)
        resize();
    afterNodeInsertion(evict);
    return null;
}

详细步骤解析

  1. 初始化表:如果哈希表还没有初始化或长度为0,则进行初始化(扩容)。
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  1. 计算索引:通过哈希值和数组长度计算出索引位置。
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  1. 插入新节点:如果索引位置为空,直接插入新节点。
  2. 处理哈希冲突:如果索引位置不为空,需要处理冲突。

检查是否存在相同的键:如果找到相同的键,替换其值。

红黑树处理:如果当前节点是红黑树节点,则调用putTreeVal方法插入。

链表处理:如果当前节点是链表节点,遍历链表插入新节点。

  1. 转换为红黑树:如果链表长度超过阈值(8)且数组长度大于 64,则将链表转换为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
  1. 更新节点值:如果存在相同的键,更新其值。
if (e != null) { // existing mapping for key
    VoldValue= e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}
  1. 调整大小:插入新节点后,增加元素数量。如果超过阈值,则进行扩容。
++modCount;if (++size > threshold)
    resize();
  1. 插入后的处理:进行一些插入后的处理操作。
afterNodeInsertion(evict);

44. JDK中常用的包⭐

java.lang

包含Java语言的核心类,不需要显式导入。

常用类:Object、String、Math、System、Thread、Exception等。

java.util

提供了集合框架、日期和时间功能、随机数生成、扫描和格式化等实用工具类。

常用类:ArrayList、HashMap、HashSet、Date、Calendar、Random、Scanner等。

java.io

提供了系统输入和输出功能,包括文件和流的操作。

常用类:File、FileInputStream、FileOutputStream、BufferedReader、BufferedWriter、InputStream、OutputStream等。

java.nio

提供了非阻塞I/O操作的类和接口,包括缓冲区、字符集、通道等。

常用类:ByteBuffer、FileChannel、Path、Files、StandardOpenOption等。

java.net

提供了用于实现网络应用程序的类,包括URL、套接字、HTTP等。

常用类:URL、URLConnection、HttpURLConnection、Socket、ServerSocket、InetAddress等。

java.sql

提供了用于访问和处理数据库的API。

常用类:Connection、Statement、PreparedStatement、ResultSet、DriverManager等。

java.time

提供了现代日期和时间API(Java 8引入)。

常用类:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Duration、Period等。

java.util.concurrent

提供了并发编程的工具类和接口。

常用类:Executor、ExecutorService、Future、CountDownLatch、Semaphore、ConcurrentHashMap等。

45. 拉链法导致的链表过深问题为什么用红黑树?⭐⭐⭐⭐⭐

在JDK 8中,HashMap采用红黑树来解决拉链法导致的链表过深问题,主要是为了提高在高冲突情况下的性能。

拉链法中的链表过深问题

在传统的拉链法中,当多个键的哈希值冲突时,这些键会被存储在同一个桶(bucket)中,形成一个链表。如果链表过长,查找、插入和删除操作的时间复杂度会退化为O(n),其中n是链表中的元素个数。这种情况下,HashMap的性能会显著下降。

红黑树的优势

红黑树是一种自平衡的二叉搜索树,具有以下特性:

  1. 自平衡:红黑树通过颜色属性(红色和黑色)和一系列的旋转操作,保证树的高度近似平衡。其高度不会超过2 * log(n),其中n是树中的节点数。
  2. 高效的查找、插入和删除:红黑树的查找、插入和删除操作的时间复杂度为O(log n),远优于链表在最坏情况下的O(n)。

为什么选择红黑树

  1. 性能优化:在链表长度较短时,链表操作的性能是可以接受的。然而,当链表长度超过一定阈值(JDK 8中为8)时,链表操作的性能会显著下降。此时,将链表转换为红黑树,可以将操作的时间复杂度从O(n)降低到O(log n),显著提高性能。
  2. 平衡性:红黑树通过自平衡机制,保证树的高度始终保持在一个较低的水平,避免了链表过长导致的性能问题。
  3. 空间效率:虽然红黑树比链表占用更多的空间(因为需要存储颜色和指针信息),但在链表长度较长时,性能的提升通常会超过空间开销的增加。

46. ConcurrentHashMap的原理?⭐⭐⭐⭐⭐

ConcurrentHashMap 是 Java 中一种高效的线程安全哈希表,主要用于在多线程环境下进行高并发的读写操作。它的设计和实现使得在大多数情况下能够提供比其他同步哈希表(如 HashMap)更高的并发性能。以下是 ConcurrentHashMap 的主要原理和机制

分段锁机制

在早期版本(Java 7及之前),ConcurrentHashMap 使用了分段锁机制(Segmented Locking)来实现高并发性。

分段锁:ConcurrentHashMap 将整个哈希表分成多个段(Segment),每个段维护一个独立的哈希表和锁。这样,在不同段上的操作可以并发进行,从而提高并发度。

// 伪代码示例
class ConcurrentHashMap<K, V> {
    Segment<K, V>[] segments;

    static class Segment<K, V> {
        final ReentrantLock lock = new ReentrantLock();
        HashEntry<K, V>[] table;
        // 其他字段和方法
    }
}

锁粒度:由于每个段都有自己的锁,只有在操作同一个段时才需要竞争锁,这大大降低了锁竞争的几率,提高了并发性能。

CAS 操作和无锁机制

在 Java 8 及之后,ConcurrentHashMap 进行了重构,摒弃了分段锁机制,转而采用了更加细粒度的锁和无锁机制(CAS 操作)。

CAS 操作:CAS(Compare-And-Swap)是一种无锁的原子操作,用于在不加锁的情况下实现线程安全。ConcurrentHashMap使用Unsafe类中的 CAS 方法来更新某些字段,从而避免了锁的开销。

// 伪代码示例
boolean casTabAt(Node<K, V>[] tab, int i, Node<K, V> c, Node<K, V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

细粒度锁:在 Java 8 中,ConcurrentHashMap使用了更加细粒度的锁(synchronizedReentrantLock),只在必要时锁定特定的桶(bin)或节点,从而进一步提高并发性能。

红黑树

为了应对哈希冲突,ConcurrentHashMap 在链表长度超过一定阈值(默认是8)时,将链表转换为红黑树,以提高查找效率。

链表:在哈希冲突较少时,使用链表存储冲突的键值对。

红黑树:当链表长度超过阈值时,转换为红黑树,以便在大量冲突时仍能保持较高的查找效率。

// 伪代码示例
if (binCount >= TREEIFY_THRESHOLD) {
    treeifyBin(tab, hash);
}

扩容机制(Rehashing)

ConcurrentHashMap 采用了渐进式扩容机制来避免扩容过程中长时间的全表锁定。

渐进式扩容:在扩容过程中,ConcurrentHashMap并不会一次性将所有数据迁移到新的哈希表中,而是采用渐进式扩容的方式,在每次插入或删除操作时,逐步迁移部分数据。

// 伪代码示例
void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {
    // 渐进式迁移数据
}

读写操作

读取操作:读取操作大部分情况下是无锁的,因为ConcurrentHashMap使用了volatile变量和 CAS 操作来保证读取的可见性和一致性。

// 伪代码示例
V get(Object key) {
    Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 其他读取逻辑
    }
    return null;
}

写入操作:写入操作则需要在必要时使用锁或 CAS 操作来保证线程安全。

// 伪代码示例
V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K, V>[] tab = table;;) {
        Node<K, V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K, V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K, V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K, V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K, V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K, V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

47. 字符串常量池是什么?⭐⭐⭐⭐⭐

字符串常量池(String Constant Pool)是Java中用于优化字符串存储和管理的一种机制。它是Java运行时环境的一部分,专门用于存储字符串字面量和某些字符串对象,以减少内存消耗和提高性能。

工作原理

字符串字面量的存储

当你在代码中使用字符串字面量(例如"hello")时,Java会首先检查字符串常量池中是否已经存在一个相同的字符串。如果存在,Java会直接引用该字符串,而不会创建新的字符串对象。如果不存在,Java会将该字符串添加到常量池中,然后引用它。

字符串对象的存储

使用new关键字创建的字符串对象(例如new String("hello"))不会自动进入字符串常量池。它们在堆内存中创建,且每次都会创建一个新的对象。

可以使用String类的intern()方法将字符串对象添加到常量池中。例如,调用str.intern()会将字符串str添加到常量池中,如果常量池中已经存在相同内容的字符串,则返回该字符串的引用。

代码 Demo

public class StringPoolExample {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = s3.intern();

        System.out.println(s1 == s2); // true, 因为s1和s2引用的是常量池中的同一个字符串
        System.out.println(s1 == s3); // false, 因为s3是通过new关键字创建的,不在常量池中
        System.out.println(s1 == s4); // true, 因为s4是通过intern()方法返回的常量池中的字符串
    }
}

优点

节省内存:字符串常量池可以确保相同内容的字符串在内存中只存储一份,减少内存消耗。

提高性能:由于字符串是不可变的,常量池可以提高字符串比较的效率(使用==进行引用比较而不是内容比较)。

48. instanceof关键字的作用⭐⭐⭐⭐⭐

instanceof关键字在Java中用于测试一个对象是否是一个特定类的实例,或者是该类的子类或实现类的实例。

它是一个二元操作符,返回一个布尔值(true或false)。

object instanceof ClassName

object:要进行类型检查的对象。

ClassName:要检查的类或接口。

主要作用

类型检查:确定对象是否是某个类的实例或是该类的子类的实例。

避免类型转换异常:在进行类型转换之前,使用instanceof可以防止ClassCastException异常。

代码 Demo

public class InstanceofExample {
    public static void main(String[] args) {
        Animal animal = new Dog();
        Dog dog = new Dog();
        Cat cat = new Cat();

        // 类型检查
        System.out.println(animal instanceof Animal); // true
        System.out.println(animal instanceof Dog);    // true
        System.out.println(animal instanceof Cat);    // false

        // 避免类型转换异常
        if (animal instanceof Dog) {
            Dog d = (Dog) animal;
            System.out.println("Animal is a Dog");
        }

        if (animal instanceof Cat) {
            Cat c = (Cat) animal; // 这段代码不会执行,因为animal并不是Cat的实例
        } else {
            System.out.println("Animal is not a Cat");
        }
    }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

注意

空值检查:如果左操作数为null,instanceof会返回false,因为null不是任何类的实例。

编译时检查:instanceof会在编译时进行类型检查,如果类型之间没有关系(例如不在同一个继承树中),编译器会报错。

代码 Demo(空值检查)

public class NullCheckExample {
    public static void main(String[] args) {
        Animal animal = null;
        System.out.println(animal instanceof Animal); // false
    }
}

代码 Demo(编译时检查)

public class CompileCheckExample {
    public static void main(String[] args) {
        String str = "hello";
        // 编译错误,因为String和Integer没有关系
        // System.out.println(str instanceof Integer); 
    }
}

49. jdk7的ConcurrentHashMap实现?⭐⭐

在JDK 7中,ConcurrentHashMap的实现与JDK 8有所不同。JDK 7中的ConcurrentHashMap使用了分段锁(Segment Locking)来实现高并发性能。

主要结构

JDK 7中的ConcurrentHashMap由以下几个主要部分组成:

  1. Segment:分段锁的核心,每个Segment是一个小的哈希表,拥有独立的锁。
  2. HashEntry:哈希表中的每个节点,存储键值对。
  3. ConcurrentHashMap:包含多个Segment,每个Segment管理一部分哈希表。

Segment 类

Segment类是ReentrantLock的子类,它是ConcurrentHashMap的核心部分。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
    }
}

HashEntry 类

HashEntry类是哈希表中的节点,存储键值对和指向下一个节点的指针。

static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    volatile HashEntry<K,V> next;
    HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
        this.key = key;
        this.hash = hash;
        this.next = next;
        this.value = value;
    }
}

ConcurrentHashMap 类

ConcurrentHashMap类包含多个Segment,每个Segment管理一部分哈希表。

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    final Segment<K,V>[] segments;
    transient Set<K> keySet;
    transient Set<Map.Entry<K,V>> entrySet;
    transient Collection<V> values;
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
    // Other fields and methods...
}

put 操作

put操作是ConcurrentHashMap的核心操作之一,以下是其简化版实现:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            } else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(key, hash, first, value);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

get 操作

get操作是ConcurrentHashMap的另一个核心操作,以下是其简化版实现:

public V get(Object key) {
    Segment<K,V> s;
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

主要特点

  1. 分段锁:ConcurrentHashMap将整个哈希表分成多个Segment,每个Segment是一个独立的小哈希表,拥有自己的锁。这样不同的线程可以并发地访问不同的Segment,显著提高并发性能。
  2. 高效并发:通过细粒度的锁机制,ConcurrentHashMap在高并发环境下表现出色,避免了全表锁的性能瓶颈。
  3. 线程安全:所有的操作都在锁的保护下进行,确保了线程安全性。

JDK 7中的ConcurrentHashMap通过分段锁机制实现高并发性能。每个Segment是一个独立的小哈希表,拥有自己的锁,允许多个线程并发地访问不同的Segment。这种设计在高并发环境下显著提高了性能,同时保证了线程安全性。

50. 什么是泛型?⭐⭐⭐⭐⭐

泛型(Generics)是Java中的一种机制,允许类、接口和方法操作指定类型的对象,而不必指定具体的类型。泛型提供了一种类型安全的方式来定义数据结构和算法,使代码更加通用和灵活,同时减少了类型转换和类型检查的错误。

主要作用

1、 类型安全:在编译时进行类型检查,避免了运行时的ClassCastException。

2、 代码重用:通过泛型,可以编写更加通用的代码,而不必为每种数据类型编写重复的代码。

泛型类

泛型类是在类定义中使用一个或多个类型参数。类型参数在类实例化时被指定。

public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello");
        System.out.println(stringBox.getValue());

        Box<Integer> integerBox = new Box<>();
        integerBox.setValue(123);
        System.out.println(integerBox.getValue());
    }
}

泛型方法

泛型方法是在方法定义中使用类型参数,类型参数在方法调用时被指定。

public class GenericMethodExample {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"Hello", "World"};

        printArray(intArray);
        printArray(stringArray);
    }
}

泛型接口

泛型接口是在接口定义中使用类型参数,类型参数在接口实现时被指定。

interface Pair<K, V> {
    K getKey();
    V getValue();
}

class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;

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

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public static void main(String[] args) {
        Pair<String, Integer> pair = new OrderedPair<>("One", 1);
        System.out.println(pair.getKey() + " : " + pair.getValue());
    }
}

泛型的限制

1、 基本类型:泛型不支持基本数据类型(如int、char等),只能使用其对应的包装类(如Integer、Character等)。

2、 类型擦除:Java的泛型是通过类型擦除实现的,这意味着在运行时,泛型类型信息会被移除,所有泛型类型都被替换为其原始类型(通常是Object)。这限制了某些操作,如创建泛型数组。

3、 静态上下文中使用泛型:不能在静态字段或静态方法中使用类型参数,因为类型参数是在实例化时才指定的,而静态成员与具体实例无关。

示例(类型擦除)

public class TypeErasureExample {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        Box<Integer> integerBox = new Box<>();

        // 在运行时,stringBox和integerBox的类型信息都被擦除,变为Box<Object>
        System.out.println(stringBox.getClass() == integerBox.getClass()); // true
    }
}

51. 说一下java8实现的concurrentHashMap?⭐⭐

Java 8 对ConcurrentHashMap进行了重新设计,取消了分段锁的机制,改用更细粒度的锁和无锁操作来提高并发性能。

数据结构

Node:基本的链表节点,存储键值对和指向下一个节点的指针。

TreeNode:用于红黑树的节点,当链表长度超过一定阈值(默认是8)时,链表会转换为红黑树。

TreeBin:红黑树的容器,管理红黑树的操作。

ForwardingNode:在扩容过程中用于指示节点已经被移动。

主要操作

put 操作:通过 CAS 操作和细粒度的锁来实现高效的并发插入和更新。

get 操作:使用无锁的方式进行查找,性能更高。

扩容:通过逐步迁移节点和协作扩容机制,提高扩容效率。

细粒度的并发控制

Java 8 中的ConcurrentHashMap采用了更细粒度的并发控制,主要通过以下方式实现:

CAS 操作:使用 CAS 操作(Compare-And-Swap)进行无锁插入和更新,减少锁竞争。

synchronized 块:在必要时对单个桶(bin)进行加锁,而不是整个段,从而进一步提高并发性。

红黑树:当链表长度超过阈值时,转换为红黑树,降低查找时间复杂度,从 O(n) 降低到 O(log n)。

Java 8 相比 Java 7 的好处

更高的并发性:Java 7 使用段级别的锁,而 Java 8 使用更细粒度的锁和无锁操作,减少了锁竞争,提高了并发性。

更好的性能:Java 8 中的get操作是无锁的,性能更高。put操作使用 CAS 和细粒度的锁,提高了插入和更新的性能。

更高效的扩容:Java 8 通过逐步迁移节点和协作扩容机制,提高了扩容效率,减少了扩容过程中对性能的影响。

更高效的查找:当链表长度超过阈值时,转换为红黑树,降低了查找时间复杂度。

52. 泛型擦除是什么?⭐⭐

泛型擦除是Java编译器在编译泛型代码时的一种机制。它的目的是确保泛型能够与Java的旧版本(即不支持泛型的版本)兼容。

在Java中,泛型信息只存在于源代码和编译时,在运行时,所有的泛型类型信息都会被擦除。这意味着在运行时,所有的泛型类型都被替换为它们的上限类型(如果没有显式指定上限,则默认为Object)。

考虑一个简单的泛型类:

public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

在编译时,泛型类型T会被擦除,并替换为它的上限类型。在这个例子中,因为没有指定上限类型,T会被替换为Object。编译后的代码大致如下:

public class Box {
    private Object value;

    public void setValue(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }
}

类型擦除的影响

运行时类型检查:由于泛型类型信息在运行时被擦除,无法在运行时获取泛型类型的信息。例如,不能使用instanceof操作符检查泛型类型。

Box<String> stringBox = new Box<>();
if (stringBox instanceof Box<String>) { // 编译错误
    // ...
}

泛型数组:不能创建泛型类型的数组,因为在运行时无法确定泛型类型。

List<String>[] stringLists = new List<String>[10]; // 编译错误

类型安全:在编译时进行类型检查,确保类型安全。然而,由于类型擦除,在某些情况下仍可能出现类型转换异常。

List<String> stringList = new ArrayList<>();
List rawList = stringList; // 允许,但不安全
rawList.add(123); // 编译时不报错,但运行时可能导致问题
String str = stringList.get(0); // 运行时抛出ClassCastException

使用限制

静态上下文中使用泛型:不能在静态字段或静态方法中使用类型参数,因为类型参数是在实例化时才指定的,而静态成员与具体实例无关。

public class GenericClass<T> {
    private static T value; // 编译错误
    
    public static T staticMethod(T param) { // 编译错误
        return param;
    }
}

泛型实例化:不能直接实例化泛型类型,因为在运行时泛型类型信息已经被擦除。

public class GenericClass<T> {
    public void createInstance() {
        T obj = new T(); // 编译错误
    }
}

53. 深拷贝和浅拷贝的区别⭐

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是对象复制的两种方式,它们在复制对象时的行为有所不同,特别是在处理包含引用类型的对象时。

浅拷贝:仅复制对象的值类型属性,对于引用类型属性,只复制引用,即新旧对象共享同一个引用类型的实例。修改新对象的引用类型属性会影响原对象。

深拷贝:不仅复制对象的值类型属性,还递归地复制引用类型属性,即新旧对象的引用类型属性指向不同的实例。修改新对象的引用类型属性不会影响原对象。

浅拷贝(Shallow Copy)

浅拷贝是指创建一个新对象,这个新对象的属性与原对象的属性具有相同的值。对于值类型(如基本数据类型),浅拷贝会复制它们的值;但对于引用类型(如对象、数组等),浅拷贝只复制引用,即新对象的属性将引用到原对象中引用的同一个对象。

class Person {
    String name;
    int age;

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

class Employee implements Cloneable {
    String position;
    Person person;

    Employee(String position, Person person) {
        this.position = position;
        this.person = person;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 浅拷贝
    }
}

public class ShallowCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person("John", 30);
        Employee original = new Employee("Manager", person);
        Employee copy = (Employee) original.clone();

        System.out.println(original.person == copy.person); // true,引用相同
        copy.person.name = "Jane";
        System.out.println(original.person.name); // "Jane",原对象也被修改
    }
}

深拷贝(Deep Copy)

深拷贝是指创建一个新对象,这个新对象与原对象具有相同的属性值,但所有引用类型的属性也会被递归地复制,即新对象与原对象的引用类型属性指向不同的对象。这样,修改新对象的引用类型属性不会影响原对象的引用类型属性。

class Person implements Cloneable {
    String name;
    int age;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Employee implements Cloneable {
    String position;
    Person person;

    Employee(String position, Person person) {
        this.position = position;
        this.person = person;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        cloned.person = (Person) person.clone(); // 深拷贝
        return cloned;
    }
}

public class DeepCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person("John", 30);
        Employee original = new Employee("Manager", person);
        Employee copy = (Employee) original.clone();

        System.out.println(original.person == copy.person); // false,引用不同
        copy.person.name = "Jane";
        System.out.println(original.person.name); // "John",原对象未被修改
    }
}

55. 什么是TreeMap?⭐⭐⭐⭐⭐

基本特点

  1. 有序性:TreeMap保证了键的自然顺序(通过Comparable接口)或通过提供的比较器(Comparator)的顺序。
  2. 红黑树:TreeMap内部使用红黑树数据结构来存储键值对,保证了插入、删除、查找等操作的时间复杂度为 O(log n)。
  3. 不允许null键:TreeMap不允许键为null,但允许值为null。
  4. 线程不安全:TreeMap不是线程安全的,如果需要在多线程环境中使用,需要通过外部同步机制来保证线程安全。

与其他集合类的比较

TreeMap vs. HashMap

TreeMap保证键的有序性,而HashMap不保证顺序。

TreeMap基于红黑树实现,操作的时间复杂度为 O(log n),而HashMap基于哈希表实现,操作的平均时间复杂度为 O(1)。

TreeMap不允许null键,而HashMap允许一个null键。

TreeMap vs. LinkedHashMap

TreeMap保证键的自然顺序或比较器的顺序,而LinkedHashMap保证插入顺序或访问顺序。

TreeMap的操作时间复杂度为 O(log n),而LinkedHashMap的操作时间复杂度为 O(1)。

适用场景

TreeMap适用于需要按键排序存储键值对的场景,例如:

实现基于范围的查询。

需要按顺序遍历键值对。

需要快速查找最小或最大键值对。

56. linkedHashMap为什么能用来做LRUCache?⭐⭐

LinkedHashMap 能用来做 LRU 缓存的关键原因在于它可以维护访问顺序,并且通过重写removeEldestEntry方法,可以轻松实现缓存的自动清理。

关键特性

访问顺序:LinkedHashMap提供了一个构造方法,可以指定是否按照访问顺序来维护键值对的顺序。当accessOrder参数设置为true时,LinkedHashMap将根据每次访问(get或put操作)来调整顺序,把最近访问的键值对移到链表的末尾。

自动清理:通过重写removeEldestEntry方法,可以在插入新键值对时自动移除最老的键值对(即链表头部的键值对),从而实现缓存的自动清理。

实现 LRU 缓存的步骤

  1. 创建一个LinkedHashMap实例,并将accessOrder参数设置为true。
  2. 重写removeEldestEntry方法,以便在缓存大小超过预定义的最大容量时自动移除最老的键值对。

代码 Demo

import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxCapacity;
    // 构造函数,初始化最大容量和访问顺序
    public LRUCache(int maxCapacity) {
        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }
    // 重写removeEldestEntry方法,当大小超过最大容量时移除最老的键值对
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxCapacity;
    }
    public static void main(String[] args) {
        // 创建一个容量为3的LRU缓存
        LRUCache<String, Integer> cache = new LRUCache<>(3);
        // 插入键值对
        cache.put("A", 1);
        cache.put("B", 2);
        cache.put("C", 3);
        // 访问键"A"(使其成为最近使用的)
        cache.get("A");
        // 插入新键值对"D",导致最老的键值对"B"被移除
        cache.put("D", 4);
        // 打印缓存内容
        System.out.println(cache); // 输出: {C=3, A=1, D=4}
    }
}

解释

构造方法:LRUCache构造方法中调用了LinkedHashMap的构造方法,并将accessOrder参数设置为true,以便按照访问顺序维护键值对的顺序。

removeEldestEntry 方法:重写了removeEldestEntry方法,当缓存的大小超过maxCapacity时返回true,从而移除最老的键值对。

使用示例:在主方法中创建了一个LRUCache实例,插入了几个键值对,并通过访问键 "A" 来改变其顺序。然后插入一个新键值对 "D",导致最老的键值对 "B" 被移除。

57. 介绍一下Java的数据结构⭐

基本数据结构

数组 (Array):固定大小的容器,用于存储相同类型的元素。数组在内存中是连续存储的,支持通过索引快速访问元素。

Java int[] numbers = new int[10]; numbers[0] = 1;

集合框架 (JCF)

JCF 提供了一组接口和类,用于管理和操作集合(如列表、集合、映射等)。

List 接口

有序集合,允许重复元素。常用实现类包括ArrayList、LinkedList和Vector。

Java List<String> list = new ArrayList<>(); list.add("A"); list.add("B");

ArrayList:基于动态数组实现,支持快速随机访问和遍历。

LinkedList:基于双向链表实现,适合频繁的插入和删除操作。

Vector:类似于ArrayList,但线程安全。

Set 接口

无序集合,不允许重复元素。常用实现类包括HashSet、LinkedHashSet和TreeSet。

Java Set<String> set = new HashSet<>(); set.add("A"); set.add("B");

HashSet:基于哈希表实现,提供快速的插入、删除和查找操作。

LinkedHashSet:保持插入顺序的HashSet。

TreeSet:基于红黑树实现,元素按自然顺序排序。

Map 接口

键值对映射,不允许重复键。常用实现类包括HashMap、LinkedHashMap和TreeMap。

Java Map\<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2);

HashMap:基于哈希表实现,提供快速的键值对存取。

LinkedHashMap:保持插入顺序的HashMap。

TreeMap:基于红黑树实现,键按自然顺序排序。

35. linkedhashmap如何保证有序性?⭐⭐

LinkedHashMap通过维护一个双向链表来保证有序性。这个双向链表记录了所有插入的键值对的顺序。根据构造方法中的参数设置,LinkedHashMap可以按插入顺序或访问顺序来维护这些键值对的顺序。

具体实现原理

  1. 双向链表:LinkedHashMap在内部维护了一个双向链表。每个节点对应一个键值对,并且包含指向前一个节点和后一个节点的引用。通过这个链表,LinkedHashMap可以快速地遍历所有键值对,保持其有序性。
  2. 插入顺序:默认情况下,LinkedHashMap按照键值对插入的顺序来维护顺序。每次插入新键值对时,它会将新节点添加到链表的末尾。
  3. 访问顺序:如果在构造方法中将accessOrder参数设置为true,LinkedHashMap将按照访问顺序来维护键值对的顺序。每次访问(get或put操作)一个键值对时,它会将对应的节点移动到链表的末尾。

代码 Demo

import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapOrderExample {
    public static void main(String[] args) {
        // 插入顺序
        LinkedHashMap<String, Integer> insertionOrderMap = new LinkedHashMap<>();
        insertionOrderMap.put("A", 1);
        insertionOrderMap.put("B", 2);
        insertionOrderMap.put("C", 3);
        System.out.println("插入顺序:");
        for (Map.Entry<String, Integer> entry : insertionOrderMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        // 访问顺序
        LinkedHashMap<String, Integer> accessOrderMap = new LinkedHashMap<>(16, 0.75f, true);
        accessOrderMap.put("A", 1);
        accessOrderMap.put("B", 2);
        accessOrderMap.put("C", 3);
        // 访问某些元素
        accessOrderMap.get("A");
        accessOrderMap.get("C");
        System.out.println("访问顺序:");
        for (Map.Entry<String, Integer> entry : accessOrderMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

解释

插入顺序

创建一个LinkedHashMap实例insertionOrderMap。

插入键值对 "A"、"B" 和 "C"。

遍历并打印键值对,顺序与插入顺序一致。

访问顺序

创建一个LinkedHashMap实例accessOrderMap,并将accessOrder参数设置为true。

插入键值对 "A"、"B" 和 "C"。

访问键 "A" 和 "C"(通过get操作)。

遍历并打印键值对,顺序按照最近访问的顺序排列。

内部机制

节点结构:LinkedHashMap的每个节点不仅包含键和值,还包含指向前一个节点和后一个节点的引用。这使得它可以高效地维护顺序。

操作调整:在每次插入或访问键值对时,LinkedHashMap会调整链表中节点的位置,以确保顺序的正确性。例如,在访问顺序模式下,每次访问一个键值对时,它会将对应的节点移动到链表的末尾。

通过这些机制,LinkedHashMap能够高效地维护键值对的有序性,无论是按插入顺序还是访问顺序。

58. try catch finally的使用⭐⭐⭐⭐⭐

try-catch-finally是 Java 中用于异常处理的结构。它允许程序员捕获和处理在程序运行过程中可能发生的异常,并在执行完异常处理后执行一些清理工作。

try {
    // 可能抛出异常的代码
} catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
} finally {
    // 无论是否发生异常,都会执行的代码
}

1、try 块:包含可能抛出异常的代码。如果在try块中发生异常,控制权将立即转移到相应的catch块。

2、catch 块:用于捕获和处理特定类型的异常。可以有多个catch块来处理不同类型的异常。每个catch块都必须处理一种特定类型的异常。catch块的参数是异常类型和异常对象。

3、finally 块:包含确保执行的代码,无论是否发生异常。通常用于清理资源,如关闭文件、释放锁等。finally块是可选的,但如果存在,它会在try块和任何相关的catch块之后执行。

59. 如何确保函数不能修改集合?⭐⭐

使用Collections.unmodifiableCollection方法

Java提供了Collections.unmodifiableCollection方法,可以将一个集合包装成一个不可修改的视图。对这个视图的修改操作将会抛出UnsupportedOperationException。

import java.util.*;
public class UnmodifiableCollectionExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        Collection<String> unmodifiableList = Collections.unmodifiableCollection(list);
        // 传递不可修改的集合给函数
        printCollection(unmodifiableList);
        // 尝试修改集合将抛出 UnsupportedOperationException
        // unmodifiableList.add("D"); // 这行代码会抛出异常
    }
    public static void printCollection(Collection<String> collection) {
        for (String item : collection) {
            System.out.println(item);
        }
    }
}

使用Collections.unmodifiable

对于特定类型的集合,如List、Set和Map,Java 提供了相应的不可修改视图方法:

Collections.unmodifiableList

Collections.unmodifiableSet

Collections.unmodifiableMap

import java.util.*;
public class UnmodifiableSpecificCollectionsExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        List<String> unmodifiableList = Collections.unmodifiableList(list);
        Set<String> set = new HashSet<>(Arrays.asList("X", "Y", "Z"));
        Set<String> unmodifiableSet = Collections.unmodifiableSet(set);
        Map<String, Integer> map = new HashMap<>();
        map.put("One", 1);
        map.put("Two", 2);
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
        // 传递不可修改的集合给函数
        printList(unmodifiableList);
        printSet(unmodifiableSet);
        printMap(unmodifiableMap);
        // 尝试修改集合将抛出 UnsupportedOperationException
        // unmodifiableList.add("D"); // 这行代码会抛出异常
        // unmodifiableSet.add("W"); // 这行代码会抛出异常
        // unmodifiableMap.put("Three", 3); // 这行代码会抛出异常
    }
    public static void printList(List<String> list) {
        for (String item : list) {
            System.out.println(item);
        }
    }
    public static void printSet(Set<String> set) {
        for (String item : set) {
            System.out.println(item);
        }
    }
    public static void printMap(Map<String, Integer> map) {
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

使用Collections.unmodifiableCollection递归包装嵌套集合

如果集合中包含嵌套集合(例如一个List中包含Set),你需要递归地将所有嵌套集合也包装成不可修改的视图。

import java.util.*;
public class UnmodifiableNestedCollectionsExample {
    public static void main(String[] args) {
        List<Set<String>> listOfSets = new ArrayList<>();
        listOfSets.add(new HashSet<>(Arrays.asList("A", "B", "C")));
        listOfSets.add(new HashSet<>(Arrays.asList("X", "Y", "Z")));
        List<Set<String>> unmodifiableListOfSets = new ArrayList<>();
        for (Set<String> set : listOfSets) {
            unmodifiableListOfSets.add(Collections.unmodifiableSet(set));
        }
        Collection<List<Set<String>>> unmodifiableCollection = Collections.unmodifiableCollection(Collections.singletonList(unmodifiableListOfSets));
        // 传递不可修改的集合给函数
        printNestedCollection(unmodifiableCollection);
        // 尝试修改集合将抛出 UnsupportedOperationException
        // unmodifiableListOfSets.get(0).add("D"); // 这行代码会抛出异常
    }
    public static void printNestedCollection(Collection<List<Set<String>>> collection) {
        for (List<Set<String>> list : collection) {
            for (Set<String> set : list) {
                for (String item : set) {
                    System.out.println(item);
                }
            }
        }
    }
}

通过以上可以确保传递给函数的集合不会被修改,从而保证集合的不可变性。

60. 运行时异常和非运行时异常的区别⭐⭐⭐⭐⭐

在 Java 中,异常分为两大类:运行时异常非运行时异常

运行时异常(Runtime Exceptions)

运行时异常是指在程序运行期间可能会发生的异常。这类异常是 RuntimeException 类及其子类的实例。

1、 无需显式捕获或声明:方法中不需要显式地捕获或声明可能抛出的运行时异常。编译器不会强制要求你处理这些异常。

2、通常是编程错误:运行时异常通常是由于编程错误引起的,例如访问空指针、数组越界、类型转换错误等。

3、常见的运行时异常

  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • ClassCastException
  • IllegalArgumentException

示例

public class RuntimeExceptionExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.println(numbers[3]); // 这会抛出 ArrayIndexOutOfBoundsException
    }
}

非运行时异常

非运行时异常是指在编译时必须处理的异常。这类异常是 Exception 类及其子类(但不包括 RuntimeException 及其子类)的实例。

1、 需要显式捕获或声明:方法中必须显式地捕获或声明可能抛出的非运行时异常。编译器会强制要求你处理这些异常。

2、 通常是可预见的异常情况:非运行时异常通常是由于合理的、可以预见的异常情况引起的,例如文件未找到、网络连接失败等。

3、 常见的非运行时异常

  • IOException
  • SQLException
  • FileNotFoundException
  • ClassNotFoundException

示例

import java.io.*;

public class NonRuntimeExceptionExample {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
            String line = reader.readLine();
            System.out.println(line);
            reader.close();
        } catch (FileNotFoundException e) {
            System.err.println("File not found: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}
今天先分享这么多,剩余的61个下篇发,需要的朋友关注点一点。

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。


王中阳讲编程
836 声望326 粉丝