大家好,我是栗筝i,从 2022 年 10 月份开始,我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程,不仅是对我个人学习历程的回顾和总结,更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。
而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。
今天与您分享的,是 Java 基础知识面试题系列的总结篇(中篇),我诚挚地希望它能为您带来启发,并在您的职业生涯中起到助益作用。衷心感谢每一位朋友的关注与支持。
String str = "aaa" 与 String str = new String("i") 一样吗 ?解答:
内部类,也称为嵌套类,是定义在另一个类中的类。根据内部类的位置和特性,我们可以将内部类分为四种:成员内部类、静态内部类、方法内部类和匿名内部类。
内部类有以下几个主要用途:
封装:内部类可以访问外部类的所有成员(包括私有成员),因此,我们可以使用内部类来隐藏复杂的实现细节,提供简单的接口。
增强封装性和可读性:内部类可以将相关的类组织在一起,这样可以使代码更易于阅读和维护。
支持多重继承:Java 不支持多重继承,但我们可以使用内部类来模拟多重继承。
实现回调:内部类常常用于实现回调。在 GUI 编程和多线程编程中,我们经常需要在某个特定的时间点执行某个特定的任务,这时我们就可以使用内部类。
总的来说,内部类是一种高级特性,它可以使我们的代码更加整洁、灵活和易于维护。
解答:
Java 内部类有四种主要类型:
成员内部类(Non-static nested class)
成员内部类是定义在另一个类的内部的类,这样的类可以无限制地访问外部类的所有成员变量和方法(即使是私有的)。因为成员内部类不是静态的,所以它不能定义任何静态数据(静态字段、静态方法、静态类),因为它与外部类的一个实例关联在一起。
静态内部类(Static nested class)
静态内部类是一个静态的、嵌套的类,它可以访问外部类的所有静态成员和方法。它通常用来对外部类进行分组,不依赖于外部类实例的状态,可以单独实例化。
局部内部类(Local classes)
局部内部类是定义在一个块内的类,比如一个方法或者一个作用域块内。它们的作用域被限制在声明它们的块中。局部内部类对外完全隐藏,除了从它们被定义的块中可以访问它们。
匿名内部类(Anonymous classes)
匿名内部类是没有名称的内部类。它们通常用来实现接口或者继承抽象类的一次性使用。它们一般用于实现接口或者继承类的非常小的局部类的工作。
下面是每种内部类类型的简单示例:
public class OuterClass {
// 成员内部类
class MemberInnerClass {
}
// 静态内部类
static class StaticNestedClass {
}
void myMethod() {
// 局部内部类
class LocalInnerClass {
}
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from an anonymous class.");
}
}).start();
}
}
每种类型的内部类都有其特定的用途和适用场景。
解答:
匿名内部类是一种没有名字的内部类,它通常用于只需要使用一次的场合。
匿名内部类通常用于以下两种类型的场合:
实现接口:匿名内部类可以在定义一个类的同时实现一个接口。例如,我们可以在创建一个线程时使用匿名内部类来实现 Runnable 接口。
继承类:匿名内部类可以在定义一个类的同时继承一个类。例如,我们可以在创建一个图形界面的按钮时使用匿名内部类来继承 ActionListener 类。
匿名内部类的语法格式如下:
new 父类名或接口名() {
// 方法重写
@Override
public void method() {
// 执行语句
}
}
匿名内部类是一种简洁的语法,它可以让我们的代码更加简洁和易于阅读。但是,由于匿名内部类没有名字,所以它只能在定义的地方使用,不能在其他地方引用,这限制了它的使用范围。
解答:
枚举类型(Enumerations)是一种特殊的数据类型,它使得一个变量只能设置为预先定义的几个值中的一个。在 Java 中,枚举定义了一个类型的实例有固定数量的静态实例。
Java 枚举是一个完整的类类型,但其语法有些特别。使用枚举可以有以下几个好处:
在 Java 中,枚举类型是通过关键字 enum 来定义的,下面是一个简单的枚举使用示例:
public class EnumExample {
// 定义一个名为Day的枚举
enum Day {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
public static void main(String[] args) {
Day today = Day.WEDNESDAY;
switch (today) {
case MONDAY:
System.out.println("Mondays are bad.");
break;
case FRIDAY:
System.out.println("Fridays are better.");
break;
case SATURDAY:
case SUNDAY:
System.out.println("Weekends are best.");
break;
default:
System.out.println("Midweek days are so-so.");
break;
}
}
}
在这个例子中,我们定义了一个名为 Day 的枚举,它有七个实例,代表了一周中的七天。在 main 方法中,我们创建了一个 Day 类型的变量 today 并将其设置为 Day.WEDNESDAY。接着,我们使用一个 switch 语句来基于 today 变量的值执行不同的代码。
枚举类型还可以有自己的构造器、方法和字段,如下所示:
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
// ... other planets ...
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
private double surfaceGravity() {
// universal gravitational constant (m3 kg-1 s-2)
final double G = 6.67300E-11;
return G * mass / (radius * radius);
}
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}
在这个例子中,每个枚举实例都有质量和半径两个属性,并且枚举 Planet 还定义了计算表面重力和物体在该行星表面的重量的方法。这表明枚举实际上是一种类,并且它们可以拥有和其他类相同的能力。
解答:
Object 类在 Java 中被视为所有类的基础和起点。这是因为在 Java 中,所有的类都默认继承自 Object 类,无论是 Java 内置的类,还是用户自定义的类。这种设计使得所有的 Java 对象都能够调用一些基本的方法,例如 equals(), hashCode(), toString() 等,这些方法都在 Object 类中被定义。
解答:
Object 类中的方法可以分为两类:native 方法和非 native 方法。
非 native 方法是:
equals():判断与其他对象是否相等。clone():创建并返回此对象的一个副本。toString():返回该对象的字符串表示。finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。native 方法是:
getClass():返回此 Object 的运行时类。hashCode():返回该对象的哈希码值。notify():唤醒在此对象监视器上等待的单个线程。notifyAll():唤醒在此对象监视器上等待的所有线程。wait(long timeout):在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。解答:
native 方法和非 native 方法的主要区别在于它们的实现方式和运行环境。
解答:
在 Java 中,== 和 equals() 方法用于比较两个对象,但它们的比较方式和使用场景有所不同。
==:对于基本数据类型,== 比较的是值是否相等;对于引用类型,== 比较的是两个引用是否指向同一个对象,即它们的地址是否相同。
equals():这是一个方法,不是操作符。它的行为可能会根据它在哪个类中被调用而变化。在 Object 类中,equals() 方法的行为和 == 相同,比较的是引用是否指向同一个对象。但是在一些类(如 String、Integer 等)中,equals() 方法被重写,用于比较两个对象的内容是否相等。因此,如果你想比较两个对象的内容是否相等,应该使用 equals() 方法。
解答:
在 Java 中,equals() 和 hashCode() 两个方法是密切相关的。如果你重写了 equals() 方法,那么你也必须重写 hashCode() 方法,以保证两个相等的对象必须有相同的哈希码。这是因为在 Java 集合框架中,特别是哈希表相关的数据结构(如 HashMap、HashSet 等)在存储和检索元素时,会使用到对象的 hashCode() 方法。
以下是 Java 中 equals() 和 hashCode() 方法的一般约定:
如果两个对象相等(即,equals(Object) 方法返回 true),那么调用这两个对象中任一对象的 hashCode() 方法都必须产生相同的整数结果。
如果两个对象不等(即,equals(Object) 方法返回 false),那么调用这两个对象中任一对象的 hashCode() 方法,不要求必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能提高哈希表的性能。
因此,如果你重写了 equals() 方法但没有重写 hashCode() 方法,可能会导致违反上述的第一条约定,从而影响到哈希表相关数据结构的正确性和性能。
解答:Java 中的字符串被设计为不可变的,这意味着一旦创建字符串对象,其内容无法更改。这个设计决策具有一些重要的优势:
线程安全性: 不可变字符串是线程安全的,因为多个线程可以同时访问一个字符串对象而无需担心并发修改导致的问题。这对于多线程应用程序来说是非常重要的。
安全性: 不可变字符串可以用作参数传递给方法,而不必担心方法在不经意间更改了字符串的内容。
性能优化: 因为字符串不可变,可以在运行时对其进行缓存,以减少内存占用和提高性能。例如,多个字符串变量可以共享相同的字符串字面值,从而节省内存。
哈希码缓存: 字符串的哈希码可以在创建时计算并缓存,这样在后续哈希比较(如在哈希表中查找字符串)时会更加高效。
字符串池: 不可变字符串使得字符串池的实现更容易,从而可以共享字符串字面值,减少内存占用。
安全性: 不可变字符串对于安全性是有帮助的。例如,当字符串用于密码或其他敏感数据时,不可变性可以确保这些数据不会在内存中不经意地被修改。
简化字符串操作: 不可变性简化了字符串操作。例如,当你连接两个字符串时,实际上是创建了一个新的字符串,而不是修改原始字符串。
尽管不可变字符串有很多优势,但它们也有一些劣势,例如在频繁修改字符串内容时可能会导致性能下降,因为每次修改都会创建新的字符串对象。为了解决这个问题,Java 提供了 StringBuilder 和 StringBuffer 等可变字符串类,以便更高效地进行字符串拼接和修改。然而,在大多数情况下,不可变字符串的优点远远超过了其劣势,因此它们在 Java中得到广泛应用。
解答:
Java 中的字符串池(String Pool)是 Java 堆内存中的一个特殊区域,用于存储所有由字面量创建的字符串对象。
当我们创建一个字符串字面量(例如,String str = "Hello";),JVM 首先会检查字符串池中是否已经存在 “Hello” 这个字符串。如果存在,那么 str 就会指向这个已存在的 “Hello” 字符串;如果不存在,JVM 就会在字符串池中创建一个新的 “Hello” 字符串,然后 str 会指向这个新创建的字符串。
通过这种方式,字符串池可以帮助我们节省内存,因为它允许相同的字符串字面量共享同一个存储空间。
String str = "aaa" 与 String str = new String("aaa") 是否一样?解答:
String str = "aaa"; 和 String str = new String("aaa"); 在 Java 中并不完全相同。
String str = "aaa";:这种方式创建的字符串会被放入字符串池中。如果字符串池中已经存在 “aaa” 这个字符串,那么 str 就会指向这个已存在的字符串;如果不存在,JVM 就会在字符串池中创建一个新的 “aaa” 字符串,然后 str 会指向这个新创建的字符串。
String str = new String("aaa");:这种方式会在堆内存中创建一个新的字符串对象,然后 str 会指向这个新创建的对象。这时,无论字符串池中是否存在 “aaa” 这个字符串,都不会影响 str 的创建。
所以,虽然这两种方式创建的字符串内容相同,但是他们在内存中的存储位置可能不同。如果你使用 == 操作符比较这两个字符串,可能会得到 false,因为 == 操作符比较的是对象的引用,而不是内容。如果你使用 equals() 方法比较这两个字符串,会得到 true,因为 equals() 方法比较的是字符串的内容
解答:
在 Java 中,主要有以下几种创建字符串的方式:
字符串字面量:这是最常见的创建字符串的方式,例如 String str = "Hello";。这种方式创建的字符串会被放入字符串池中。
使用 new 关键字:例如 String str = new String("Hello");。这种方式会在堆内存中创建一个新的字符串对象。
通过字符数组:例如 char[] array = {'H', 'e', 'l', 'l', 'o'}; String str = new String(array);。这种方式会创建一个新的字符串,内容是字符数组的内容。
通过 StringBuilder 或 StringBuffer:例如 StringBuilder sb = new StringBuilder("Hello"); String str = sb.toString();。这种方式可以创建一个可变的字符串,然后再转换为不可变的 String。
通过 String.format() 方法:例如 String str = String.format("Hello %s", "World");。这种方式可以创建一个格式化的字符串。
以上就是在 Java 中创建字符串的主要方式。
解答:
String:在 Java 中,String 是不可变的,也就是说一旦一个 String 对象被创建,我们就不能改变它的内容。每次对 String 类型进行修改,都会生成一个新的 String 对象。这在需要大量修改字符串时,会导致内存的大量占用和效率的降低。
StringBuffer:StringBuffer 是线程安全的可变字符序列。每个方法都是同步的,可以被多个线程安全地调用。但是,这种线程安全带来的缺点是效率相对较低。
StringBuilder:StringBuilder 是一个可变字符序列,它提供了 append、insert、delete、reverse、setCharAt 等方法来修改字符串。与 StringBuffer 相比,StringBuilder 不是线程安全的,因此在单线程环境下,StringBuilder 的效率更高。
总结一下,他们之间的区别主要在于:
String 是不可变的,而 StringBuffer 和 StringBuilder 是可变的。StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。StringBuilder。在多线程环境下,应使用 StringBuffer 来确保线程安全。如果字符串不需要修改,那么使用 String 是最好的选择。解答:
深拷贝和浅拷贝是编程中常见的两种复制对象的方式,主要区别在于是否复制对象内部的引用对象。
浅拷贝(Shallow Copy):当进行浅拷贝时,如果对象中的字段是基本类型,会直接复制其值;如果对象中的字段是引用类型,那么只复制其引用,而不复制引用指向的对象。因此,原对象和拷贝对象会共享同一个引用对象。这就意味着,如果其中一个对象改变了这个引用对象的内容,那么另一个对象的这个引用对象的内容也会随之改变。
深拷贝(Deep Copy):当进行深拷贝时,无论对象中的字段是基本类型还是引用类型,都会创建一个新的副本。对于引用类型,会复制引用指向的对象,而不仅仅是复制引用。因此,原对象和拷贝对象不会共享任何一个引用对象。这就意味着,无论哪一个对象改变了引用对象的内容,都不会影响到另一个对象。
需要注意的是,实现深拷贝可能会比较复杂,特别是当对象的引用结构很复杂时,例如存在循环引用。此外,深拷贝可能会消耗更多的计算和存储资源。
解答:
在 Java 中,clone() 方法默认进行的是浅拷贝。
这意味着,如果你的对象中包含了对其他对象的引用,那么 clone() 方法只会复制这个引用,而不会复制引用指向的对象。因此,原对象和克隆对象会共享这个引用指向的对象,这就是所谓的浅拷贝。
如果你想实现深拷贝,即完全复制一个新的对象,包括其引用的所有对象,那么你需要重写 clone() 方法,手动复制这些对象。
解答:
在实现深拷贝时,如果遇到循环引用,需要特别小心,否则可能会导致无限递归,最终导致栈溢出。
处理循环引用的一种常见方法是使用一个哈希表来跟踪已经复制过的对象。具体来说,每当你复制一个对象时,都将原对象和复制的新对象放入哈希表中。然后,在复制一个对象之前,先检查这个对象是否已经在哈希表中。如果已经在哈希表中,那么就直接返回哈希表中的复制对象,而不再进行复制。
以下是一个简单的示例:
public class Node {
public Node next;
// ...
}
public class DeepCopy {
private HashMap<Node, Node> visited = new HashMap<>();
public Node clone(Node node) {
if (node == null) {
return null;
}
if (visited.containsKey(node)) {
return visited.get(node);
}
Node cloneNode = new Node();
visited.put(node, cloneNode);
cloneNode.next = clone(node.next);
return cloneNode;
}
}
在这个示例中,DeepCopy 类使用了一个 visited 哈希表来跟踪已经复制过的 Node 对象。在 clone() 方法中,每次复制一个 Node 对象之前,都会先检查这个对象是否已经在 visited 哈希表中。这样就可以避免因为循环引用而导致的无限递归。
解答:
在实现深拷贝时,对于数组和集合类的处理需要特别注意,因为它们都可能包含引用类型的元素。
数组:如果数组的元素是基本类型,那么可以直接使用 clone() 方法或 System.arraycopy() 方法来复制数组。如果数组的元素是引用类型,那么需要遍历数组,对每个元素进行深拷贝。
MyClass[] copy = new MyClass[array.length];
for (int i = 0; i < array.length; i++) {
copy[i] = array[i].clone();
}
集合类:对于集合类,如 ArrayList、HashSet 等,需要创建一个新的集合,然后遍历原集合,对每个元素进行深拷贝,并添加到新集合中。
ArrayList<MyClass> copy = new ArrayList<>();
for (MyClass item : list) {
copy.add(item.clone());
}
需要注意的是,实现深拷贝可能会比较复杂,特别是当对象的引用结构很复杂时,例如存在循环引用。此外,深拷贝可能会消耗更多的计算和存储资源。
解答:
Cloneable 接口在 Java 中被称为标记接口(Marker Interface),它本身并没有定义任何方法,但是它对于 Java 的对象克隆机制来说非常重要。
当一个类实现了 Cloneable 接口后,它就表明它的对象是可以被克隆的,即它的 clone() 方法可以被合法地调用。如果一个类没有实现 Cloneable 接口,但是调用了它的 clone() 方法,那么将会在运行时抛出 CloneNotSupportedException 异常。
实现 Cloneable 接口的目的是为了让 Object 的 clone() 方法知道它可以对这个类的对象进行字段对字段的复制。
需要注意的是,虽然 Cloneable 接口本身并没有定义任何方法,但是实现 Cloneable 接口的类通常需要重写 Object 类的 clone() 方法,以提供公开的克隆方法并实现类特定的克隆行为,例如深拷贝。
解答:
Cloneable 接口被称为标记接口,是因为它本身并没有定义任何方法,它的作用主要是为了标记一个类的对象可以被克隆。
在 Java 中,Cloneable 接口的主要作用是告诉 Object 的 clone() 方法,它可以对实现了 Cloneable 接口的类的对象进行字段对字段的复制。
如果一个类没有实现 Cloneable 接口,但是调用了它的 clone() 方法,那么将会在运行时抛出 CloneNotSupportedException 异常。
因此,Cloneable 接口虽然没有定义任何方法,但是它对于 Java 的对象克隆机制来说非常重要,它是一种标记,表明一个类的对象可以被克隆。