JAVA SE下——八股2
6、重载和重写的区别?(655/1759=37.2%)
重载常用于提供多种调用方式,而重写则用于实现多态性,增强代码的灵活性和可扩展性。接下来我会从6个方面详细说一下它们的区别。
第一是==发生位置的不同,重载发生在同一个类中,而重写发生在父子类之间== 。
第二是==方法签名的不同,重载要求方法名相同,但参数列表必须不同。重写要求方法名和参数列表完全相同。==
第三是==返回值类型的不同,重载的返回值类型可以不同,而重写的返回值类型必须相同或是父类返回值类型的子类型==。
第四是==访问修饰符的不同,重载对访问修饰符没有限制,而重写的访问修饰符不能比父类更严格==。
第五是==异常声明的不同,重载对异常声明没有限制,而重写时,子类方法抛出的异常不能比父类方法抛出的异常范围更大==。
第六是==绑定关系的不同,重载是静态绑定 ,编译时确定调用哪个方法,而重写是动态绑定 ,运行时根据对象的实际类型决定调用哪个方法。==
拓展:
1.什么是重载?
重载是指在同一个类中,允许存在多个同名方法,但这些方法的参数列表必须不同。重载的核心在于方法签名的不同,而返回值类型不影响重载。
当定义一个重载方法时,
首先,方法名必须相同。
其次,参数列表必须不同,包括参数的数量、类型或顺序。
然后,返回值类型可以相同也可以不同,但它不影响重载的判断。
最后,访问修饰符(如 public、private 等)和异常声明也不影响重载。
2.什么是重写?
重写是指子类对父类中已有的方法进行重新定义,以提供特定的实现。重写的核心在于继承关系,并且要求方法签名完全一致。
当定义一个重写方法时,
首先,方法名必须与父类中的方法名相同。
其次,参数列表必须与父类中的方法完全一致。
然后,返回值类型必须相同或是父类返回值类型的子类型(协变返回类型)。
最后,访问修饰符不能比父类更严格(例如,父类方法是 protected,子类方法可以是 protected 或 public,但不能是 private)。
7、= =(双等号)和 equals 的区别?(667/1759=37.9%)
== 和 equals 是 Java 中用于比较的两种方式,接下来我会从5个方面来说一下它们的区别。
第一个==是比较内容上,** 比较的是内存地址(引用类型)或实际值(基本数据类型)**,而equals 比较的是逻辑上的相等性,具体取决于类是否重写了 equals 方法。
第二个是==适用范围上, 可用于==基本数据类型和引用数据类型==,而 equals 只能用于引用数据类型。
第三个==是默认行为上, 始终比较的是内存地址或实际值,而equals 在未重写时与 == 行为一致,但在某些类中(如 String、Integer 等)被重写以实现内容比较。
第四个是==可扩展性上, 是==操作符,无法被修改或扩展,而equals 是方法,可以在自定义类中重写==以实现特定的比较逻辑。
第五个是==性能上, 性能更高,因为它直接比较内存地址或值,而equals 性能可能较低,尤其是在复杂对象中需要逐个比较属性值。
拓展:
1.什么是 = =?
== 是一个操作符,用于比较两个对象或基本数据类型的值是否相等。它的核心在于直接比较内存地址或数值本身。
当使用 == 进行比较时,首先,对于基本数据类型(如 int、double 等),== 比较的是它们的实际值。其次,对于引用数据类型(如对象、数组等),== 比较的是它们的内存地址,即判断两个引用是否指向同一个对象。
2.什么是 equals?
equals 是一个方法,定义在 Object 类中,默认实现与 == 类似,==比较的是对象的内存地址。但在某些类(如 String、Integer 等)中,equals 方法被重写以实现逻辑上的相等==性。
当使用 equals 进行比较时,
首先,如果类没有重写 equals 方法,则默认行为是调用 Object 类的实现,比较的是内存地址。
其次,如果类重写了 equals 方法(如 String 或自定义类),则会根据重写的逻辑来判断两个对象是否“相等”。
然后,常见的重写逻辑是基于对象的属性值进行比较,而不是内存地址。
3.hashCode() 的作用
hashCode() 是 Java 中 Object 类的一个方法,用于返回对象的哈希码(Hash Code),它是一个整数值。这个哈希码的==主要作用是确定对象在基于哈希表的数据结构中的存储位置。==
(1) 哈希码的核心作用:定位对象
快速定位:
在基于哈希表的数据结构(如 HashMap、HashSet 和 Hashtable)中,hashCode() 被用来计算对象的存储位置。
哈希表通过将对象的==哈希值映射到特定的“桶”==(Bucket)中,从而实现高效的插入、查找和删除操作。
这种机制使得==哈希表的时间复杂度通常为 O(1)==,即常数时间复杂度,极大提升了性能。
散列冲突(哈希冲突)处理:
不同的对象可能会生成相同的哈希码(即哈希冲突)。为了应对这种情况,哈希表会采用链表、红黑树等数据结构来存储多个对象,并通过 equals() 方法进一步比较这些对象是否真正相等。
(2) 示例:HashSet 插入过程
假设我们有一个 HashSet,其中已经存储了 1000 个元素。当插入第 1001 个元素时,HashSet 的工作流程如下:
-
计算哈希码:首先调用新元素的 hashCode() 方法,计算出该元素的哈希码。
-
确定存储位置:根据哈希码计算出该元素在底层数组中的索引位置。
-
处理冲突:如果该位置已经有其他元素(即发生==哈希冲突==),则通过 equals() 方法逐一比较这些元素,判断是否存在重复。
-
插入元素:如果没有重复,则将新元素插入;否则忽略。
通过这种方式,HashSet 能够高效地插入和去重,避免了逐个比较所有元素的低效操作。
4.为什么散列表(Hash Table)需要哈希码?
为了更好地理解 hashCode() 的作用,我们需要了解散列表的基本原理:
(1)散列表的本质:散列表是一种基于数组的数据结构,通过哈希函数将键映射到数组的某个位置。键的哈希码决定了它在数组中的索引位置。
(2)快速检索:当我们需要==根据键查找对应的值时,可以通过哈希函数==直接定位到数组中的目标位置,从而避免了逐个遍历的低效操作。
(3)冲突解决:如果两个键的哈希码相同(即哈希冲突),散列表会通过==链表或红黑树等方式存储多个键值对==,并通过 equals() 方法进一步比较这些键是否真正相等。
8、什么是泛型?有什么作用?(501/1759=28.5%)
泛型(Generics)是 Java 中一种重要的编程特性,它允许我们在定义类、接口和方法时使用类型参数,从而==使代码更加通用、灵活且安全==。接下来我会详细解释泛型的定义和作用。
首先说一下==什么是泛型,==
泛型是一种在==编译时提供类型安全检查的机制,它允许我们将类型作为参数传递给类、接口或方法,从而避免硬编码具体的类型。通过泛型,我们可以编写适用于多种数据类型的代码,同时确保类型安全==。
泛型的作用主要有4点
-
第一点是==提高代码的复用性==,它允许我们编写与类型无关的通用代码。
-
第二点是==增强类型安全性==,在没有泛型的情况下,集合类(如 ArrayList)默认存储的是 Object 类型,取出元素时需要手动进行类型转换,容易引发 ClassCastException。而泛型在编译时就会进行类型检查,避免了运行时的类型错误。**
-
第三点是==简化代码,使用泛型后,我们无需显式地进行类型转换==,减少了冗余代码,提高了代码的可读性和维护性。**
-
第四点是==支持复杂的类型约束,泛型可以通过通配符(==如 ? extends T 和 ? super T)实现更复杂的类型限制,满足特定场景下的需求。**
? extends T:表示类型是 T 或 T 的子类型(即 T 的派生类)? super T:表示类型是 T 或 T 的父类型(即 T 的祖先类)
拓展:
1.如何定义泛型
泛型类:通过在类名后添加类型参数(如 )来定义泛型类。
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
泛型方法:通过在方法==返回值前添加类型参数(如 )来定义泛型方法==。
public static void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
2.如何使用泛型(其中之一)
实例化泛型类时,指定具体的类型参数。
Pair<String, Integer> pair = new Pair<>(“Age”, 25);
String key = pair.getKey();
Integer value = pair.getValue();
调用泛型方法时,编译器会自动推断类型参数。
Integer[] numbers = {1, 2, 3};
printArray(numbers); // 编译器推断 T 为 Integer
3.泛型为什么被引入
JDK 1.5引入,泛型解决了一个类每次只能传入同一种对象的问题,还解决了编译能过,运行不能过的问题,泛型的原理是擦除机制,如果 Java 在运行时保留泛型类型信息,那么每个泛型类型的实例都将占用额外的内存,并且 JVM 需要维护这些类型信息。类型擦除使得在运行时不需要保存额外的类型信息,从而节省了内存开销。类型擦除还避免了为每个泛型类型创建多个类的需求,从而减少了类加载器的负担。
9、什么是反射?应用?(607/1759=34.5%)
反射(Reflection)是 Java 中一种强大的机制,它允许==程序在运行时动态地获取类的信息并操作类的属性、方法和构造器==。接下来我会详细解释反射的定义和应用场景。
首先说一下==什么是反射,==
反射是一种在运行时动态获取类信息的能力。通过反射,我们可以在程序运行时加载类、获取类的结构(如字段、方法、构造器等),甚至可以调用类的方法或修改字段的值。
其次,反射主要应用在这5个场景,框动注插测
第一个是框架开发,很多 Java 框架都有使用反射,比如如 Spring、Hibernate 等。
第二个是动态代理,动态代理是反射的一个重要应用,常用于 AOP(面向切面编程)。通过反射,我们可以在运行时动态生成代理类,拦截方法调用并添加额外逻辑。
第三个是注解处理,注解本身不会对程序产生任何影响,但通过反射,我们可以在运行时读取注解信息并执行相应的逻辑。
第四个是插件化开发,在某些场景下,我们需要动态加载外部的类或模块。反射可以帮助我们在运行时加载这些类并调用其方法,从而实现插件化开发。
第五个是测试工具,单元测试框架(如 JUnit)利用反射来发现和运行测试方法,而无需手动指定每个测试用例。
拓展:
1.为什么引入反射
(1)问题背景:动态加载类和调用方法
在传统的 Java 编程中,所有的类和方法调用必须在编译时确定。例如:
MyClass obj = new MyClass();
obj.myMethod();
这种模式虽然安全且高效,但在某些场景下显得不够灵活。例如:
问题:
假设我们==正在开发一个插件系统==,允许用户通过配置文件指定需要加载的类和调用的方法。例如,配置文件的内容如下:
className=com.example.MyPlugin
methodName=execute
我们的程序需要根据配置文件的内容==动态加载 com.example.MyPlugin 类==,并调用其 execute 方法。
(2)传统方式的局限性:
在传统编程中,我们必须提前知道 MyPlugin 类的存在,并将其硬编码到程序中。
如果用户更换了插件(即更改了配置文件中的类名或方法名),我们需要重新编译整个程序,这显然不符合动态性和灵活性的要求。
(3)解决方案:使用==反射实现动态加载和调用==
Java 的反射机制允许程序在运行时动态加载类并调用其方法,从而解决了上述问题。
动态加载类
反射提供了 Class.forName(String className) 方法,可以根据类的全限定名(Fully Qualified Name)动态加载类。
示例代码:
String className = “com.example.MyPlugin”; // 从配置文件读取
Class < ?> clazz = Class.forName(className); // 动态加载类
Object instance = clazz.getDeclaredConstructor().newInstance(); // 创建实例
动态调用方法
反射还提供了 Method 类,用于表示类中的方法,并允许在运行时调用这些方法。
示例代码:
String methodName = “execute”; // 从配置文件读取
Method method = clazz.getMethod(methodName); // 获取方法
method.invoke(instance); // 调用方法
(4)为什么反射能解决这个问题?
动态性:反射允许程序在运行时根据外部输入(如配置文件)动态加载类和调用方法,而无需在编译时确定。这使得程序可以适应不断变化的需求,例如用户更换插件或扩展功能。
灵活性==:反射不依赖于具体的类或方法名称,而是通过字符串参数动态操作类和方法==。这种灵活性非常适合框架开发、插件系统等需要高度可扩展性的场景。
解耦:使用反射后,程序与具体的类和方法解耦,用户只需提供符合约定的类和方法即可,无需修改主程序代码。
2.反射的优点和缺点
(1)反射的优点:
提供了动态性和灵活性,适用于框架开发、插件系统等需要高度可扩展性的场景。
实现了解耦设计,提高了代码的可维护性和扩展性。
在调试和测试中非常有用,可以访问私有成员。
(2)反射的缺点:
性能开销较大,不适合高频调用。
存在安全性问题,可能绕过访问控制机制。
可读性和可维护性较低,代码难以追踪和调试。
编译时检查失效,增加了运行时错误的风险。
10、请说说 StringBuffer 的特点(383/1759=21.8%)
StringBuffer 是 Java 中用于处理可变字符串的一个重要类,它在==多线程环境下表现出色,能够高效地进行字符串拼接和修改操作==。接下来我会详细解释 StringBuffer 的定义以及它的特点。
首先说一下==什么是 StringBuffe==r?
StringBuffer 是一个可变的字符序列,与 String 不同,StringBuffer 的内容是可以被修改的。它的核心特点是线程安全和高效的字符串操作。
然后说一下 StringBuffer 的4个特点
- 第一个是它==具有可变性,我们可以在原有对象上直接修改字符串内容,而无需创建新的对象==。
- 第二个它是==线程安全的==,StringBuffer 的所有方法都通过 synchronized 关键字修饰,因此它是线程安全的。 ==在多线程环境下,多个线程可以同时操作同一个 StringBuffer 对象,而不会引发数据竞争==或不一致问题。
- 第三个是==性能相对较好,StringBuffer 内部使用一个可扩容的字符数组来存储数据,当容量不足时会自动扩展。相比于 String 的不可变性(每次修改都会生成新对象==),StringBuffer 在频繁修改字符串时性能更高。
而相比于非线程安全的 StringBuilder ,性能略低。 - 第四个==是包含丰富的 API==,比如:append():追加内容到字符串末尾。 insert():在指定位置插入内容。delete():删除指定范围的内容。 ==reverse():反转==字符串内容。 toString():将 StringBuffer 转换为 String。
拓展:
1.StringBuffer 的应用场景
StringBuffer 的特点决定了它在以下场景中非常适用:
-
多线程环境下的字符串操作:当需要在多线程环境中频繁修改字符串时,StringBuffer 是首选,因为它提供了线程安全性。
-
频繁的字符串拼接操作:在需要大量拼接字符串的场景下(如日志记录、文件读写等),StringBuffer 比 String 更高效,因为它避免了频繁创建新对象的开销。
-
复杂的字符串修改操作:如果需要对字符串进行插入、删除、反转等复杂操作,StringBuffer 提供了丰富的 API 来满足这些需求。
2.String、StringBuffer、StringBuilder 的区别?
| 特性 | String | StringBuffer | StringBuilder |
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全(不可变性保证) | 线程安全(方法加同步锁) | 非线程安全 |
| 性能 | 频繁修改时性能低 | 性能较高,但低于 StringBuilder | 单线程环境下性能最高 |
| 使用场景 | 字符串内容不经常变化 | ==多线程环境==下频繁修改字符串 | ==单线程==环境下频繁修改字符串 |
以下代码展示了三者在性能上的差异:
public class StringComparison {
public static void main(String[] args) {
int n = 100000;
// 使用 String
long startTime = System.currentTimeMillis();
String str = "";
for (int i = 0; i < n; i++) {
str += "a"; // 每次都会生成新的 String 对象
}
long endTime = System.currentTimeMillis();
System.out.println("String 耗时:" + (endTime - startTime) + "ms");
// 使用 StringBuffer
startTime = System.currentTimeMillis();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < n; i++) {
sb.append("a"); // 修改原有对象
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer 耗时:" + (endTime - startTime) + "ms");
// 使用 StringBuilder
startTime = System.currentTimeMillis();
StringBuilder sbuilder = new StringBuilder();
for (int i = 0; i < n; i++) {
sbuilder.append("a"); // 修改原有对象
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder 耗时:" + (endTime - startTime) + "ms");
}
}
运行结果(示例)
String 耗时:1200ms
StringBuffer 耗时:10ms
StringBuilder 耗时:5ms
从结果可以看出:
String 的性能最差,因为每次修改都会生成新对象。
StringBuffer 和 StringBuilder 的性能显著优于 String。
StringBuilder 的性能略高于 StringBuffer,因为没有同步开销。
更多推荐



所有评论(0)