接口和抽象类的区别
接口 | 抽象类 | |
---|---|---|
方法 | 抽象方法 | 既可以有抽象方法,也可以有普通方法 |
关键字修饰 | interface | abstract |
定义常量变量 | 只能定义静态常量 | 成员变量 |
子类方法 | 所有方法必须实现 | 实现所有的抽象方法 |
子类继承 | 多继承 | 单继承 |
构造方法 | 不能有构造方法 | 可以有构造方法 |
接口实现 | 只能继承接口,不能实现接口 | 可以实现接口,并且不实现接口中的方法 |
Java 中的继承和 C++ 有什么不同
Java 中的继承和 C++ 中的继承在概念上是相似的,但在语法和实现细节上有一些不同之处。以下是 Java 中的继承和 C++ 中的继承的主要不同点:
-
语法差异:
- 在 Java 中,继承使用关键字
extends
,例如:class ChildClass extends ParentClass {...}
。 - 在 C++ 中,继承使用关键字
:
,例如:class ChildClass : public ParentClass {...}
。
- 在 Java 中,继承使用关键字
-
默认访问控制:
- 在 Java 中,如果子类没有显式指定访问修饰符,子类只能继承父类的
public
和protected
成员,而不能继承private
成员。 - 在 C++ 中,默认情况下子类继承父类的所有成员,无论是
public
、protected
还是private
。
- 在 Java 中,如果子类没有显式指定访问修饰符,子类只能继承父类的
-
多继承:
- Java 不支持多继承,一个类只能继承自一个父类。
- C++ 支持多继承,一个类可以同时继承自多个父类。
-
虚函数和方法重写:
- 在 Java 中,所有的成员函数默认都是虚函数(即可以被子类重写),如果不希望子类重写,可以使用
final
关键字修饰。 - 在 C++ 中,默认情况下成员函数不是虚函数,需要使用
virtual
关键字声明虚函数,而且在子类中重写父类的虚函数时,不需要显式添加virtual
关键字。
- 在 Java 中,所有的成员函数默认都是虚函数(即可以被子类重写),如果不希望子类重写,可以使用
-
构造函数和析构函数:
- 在 Java 中,子类的构造函数必须调用父类的构造函数,可以使用
super()
关键字来调用父类的构造函数。 - 在 C++ 中,子类的构造函数可以显式调用父类的构造函数,也可以省略,如果省略,则默认调用父类的默认构造函数;析构函数也类似。
- 在 Java 中,子类的构造函数必须调用父类的构造函数,可以使用
总的来说,Java 中的继承和 C++ 中的继承在语法和部分实现细节上有所不同,但在概念上都是为了实现代码的重用性和扩展性而设计的。
Java 中有哪些数据结构?用过 HashMap 吗,说一下 HashMap 底层实现
Java 中常用的数据结构包括:
-
数组(Array):一组按顺序存储的相同类型元素的集合,通过索引访问元素。
-
链表(Linked List):一种线性表数据结构,由节点组成,每个节点包含数据元素和指向下一个节点的引用。
-
栈(Stack):一种后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。
-
队列(Queue):一种先进先出(FIFO)的数据结构,支持在队尾插入元素,在队头删除元素。
-
堆(Heap):一种特殊的树形数据结构,通常用于实现优先队列。
-
树(Tree):一种层次结构的数据结构,包括二叉树、平衡二叉树、红黑树等。
-
图(Graph):一种非线性的数据结构,由节点(顶点)和边组成,用于描述各种复杂关系。
-
哈希表(Hash Table):一种通过哈希函数将键映射到值的数据结构,实现了键值对的存储和快速检索。
HashMap 是 Java 中常用的哈希表实现之一,其底层实现主要包括数组和链表(或红黑树)。HashMap 的工作原理是将键值对映射到数组的一个位置上,通过哈希函数计算键的哈希码,然后将键值对存储在数组中对应位置的链表(或红黑树)中。当发生哈希冲突时,即多个键映射到了同一个数组位置上,HashMap 使用链表(或红黑树)来解决冲突,将冲突的键值对按顺序存储在链表中。在 JDK 8 中,当链表长度超过一定阈值时,链表会转换成红黑树,以提高查找、插入和删除操作的效率。
HashMap 在 JDK 8 中引入了一种称为“拉链法”的解决冲突方法,通过数组和链表(或红黑树)结合的方式来提高插入、查找和删除操作的性能,是 Java 中最常用的数据结构之一。
Java 中用的是值传递还是引用传递?
在 Java 中,参数传递采用的是值传递(pass by value)。
在 Java 中,所有的参数传递都是将参数的值复制一份传递给方法的参数,而不是传递参数的引用。具体来说:
-
对于基本数据类型:例如整型、浮点型、布尔型等,传递的是值本身的副本,对方法内部的参数进行修改不会影响到原始变量的值。
-
对于引用类型:例如对象、数组等,传递的是引用的副本,即传递的是对象的地址或引用,而不是对象本身。这意味着,虽然在方法内部可以修改对象的状态,但对于对象的重新赋值或修改引用并不会影响到原始对象。
举个例子:
public class Main {public static void main(String[] args) {int x = 10;changeValue(x);System.out.println("x after changeValue: " + x); // 输出:x after changeValue: 10StringBuilder sb = new StringBuilder("Hello");changeReference(sb);System.out.println("sb after changeReference: " + sb.toString()); // 输出:sb after changeReference: Hello World}public static void changeValue(int x) {x = 20; // 修改方法参数的值}public static void changeReference(StringBuilder sb) {sb.append(" World"); // 修改方法参数的引用对象}
}
在上述示例中,尽管在 changeValue
方法中修改了参数 x
的值为 20,但在 main
方法中输出 x
的值仍然是 10。而在 changeReference
方法中修改了 StringBuilder
对象的内容,main
方法中输出的结果确实被修改了。这反映了 Java 中参数传递采用的是值传递,但对于引用类型,传递的是引用的副本。
面向过程和面向对象有什么区别?
面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming,OOP)是两种不同的编程范式,它们有以下主要区别:
-
核心思想:
- 面向过程编程:将问题划分为一系列的步骤,然后使用函数来实现每个步骤,强调的是过程和行为。
- 面向对象编程:将问题划分为一组对象,每个对象包含数据和方法,强调的是对象和数据。
-
编程单位:
- 面向过程编程以过程或函数作为编程的基本单位,程序由一系列函数组成,函数之间通过参数传递数据。
- 面向对象编程以对象作为编程的基本单位,程序由一组对象组成,对象之间通过消息传递来进行通信。
-
数据和行为的组织方式:
- 面向过程编程将数据和行为分开,数据和函数是分离的,数据可以被多个函数共享。
- 面向对象编程将数据和行为封装在对象中,对象是数据和行为的集合,数据和方法是紧密相关的,对象对外部隐藏了内部实现细节。
-
继承和多态:
- 面向对象编程支持继承和多态的特性,通过继承可以实现代码的重用和扩展,通过多态可以实现代码的灵活性和可扩展性。
- 面向过程编程通常不支持继承和多态,代码的复用和扩展主要依靠函数的调用和组合。
-
适用场景:
- 面向过程编程适用于简单的、线性的问题,适合于需要执行一系列步骤的程序。
- 面向对象编程适用于复杂的、交互式的系统,适合于需要对数据和行为进行抽象和建模的程序。
总的来说,面向过程编程和面向对象编程是两种不同的编程思想和方法,各有其优缺点和适用场景。面向对象编程通过封装、继承和多态等特性提供了更加灵活、可扩展和易维护的编程方式,已经成为了主流的编程范式。
final、finally、finalize 的区别?
final
、finally
和 finalize
是 Java 中的三个不同的关键字,它们在语法和用途上有着不同的含义和作用:
-
final:
final
是 Java 中的关键字,用于修饰类、方法和变量,表示不可改变的意思。- 当用
final
修饰一个类时,表示该类是一个不可继承的最终类,不能被其他类继承。 - 当用
final
修饰一个方法时,表示该方法是一个最终方法,子类不能重写该方法。 - 当用
final
修饰一个变量时,表示该变量是一个常量,一旦被赋值后就不能再改变其值。
-
finally:
finally
是 Java 中的关键字,用于定义在try-catch
结构中的代码块,在try
或catch
中的代码执行完毕后,无论是否发生异常,finally
中的代码块都会被执行。finally
常用于资源的释放,例如关闭文件、关闭数据库连接等,确保资源被正确释放,避免资源泄漏。
-
finalize:
finalize
是 Object 类中的一个方法,用于在对象被垃圾回收器回收之前进行资源释放和清理工作。- 在 Java 中,垃圾回收器负责回收不再使用的对象,当对象被标记为垃圾时,垃圾回收器会调用其
finalize
方法,进行一些必要的清理操作。 - 但
finalize
方法的执行时间是不确定的,并不能保证对象什么时候被垃圾回收器回收,因此不建议过度依赖finalize
方法进行资源释放,而应该显式地使用finally
块或者try-with-resources
语句来进行资源的释放。
什么是序列化?什么是反序列化?
序列化(Serialization)是指将对象转换为字节流的过程,以便将其存储到文件中、通过网络进行传输,或者将其保存在内存中。序列化的过程将对象的状态转换为字节序列,可以在需要时重新创建对象。
反序列化(Deserialization)则是指将序列化后的字节流重新转换为对象的过程。反序列化的过程将字节序列转换为对象的状态,从而使得我们能够恢复原始对象。
什么是不可变类?
不可变类(Immutable Class)是指一旦创建后,其状态(即对象的属性值)就不能被修改或改变的类。换句话说,不可变类的对象一经创建,其状态就不可再被修改,任何对其状态的修改都会导致创建一个新的对象。
不可变类具有以下特点:
-
状态不可变性(State Immutability):不可变类的属性值在对象创建后不能被修改。
-
实例共享(Instance Sharing):由于不可变类的对象状态不可变,因此可以安全地在多个线程之间共享,无需担心线程安全问题。
-
线程安全性(Thread Safety):由于不可变类的对象状态不可变,因此不可变类是线程安全的,无需额外的同步措施。
-
简化并发编程(Simplify Concurrent Programming):不可变类简化了并发编程,因为无需担心对象的状态被并发修改而导致的问题。
-
更易于理解和调试(Easier to Understand and Debug):不可变类更容易理解和调试,因为它的状态不会发生变化,可以避免由于状态变化而导致的复杂性。
典型的不可变类包括 String
、Integer
、BigInteger
、BigDecimal
等 JDK 中的一些基本类型包装类,以及很多其他的第三方库中的不可变类。在设计和实现自己的类时,如果希望提供不可变性,可以通过将属性设置为私有并提供只读方法,或者在构造函数中初始化属性并不提供修改方法等方式来实现。
为什么 Java 中 String 是不可变类?
-
安全性(Security):字符串常常用于存储敏感信息,如密码、密钥等。如果字符串是可变的,那么在传递字符串时可能会被意外地修改,从而导致安全问题。通过将字符串设置为不可变,可以确保其值在创建后不会被修改,从而提高安全性。
-
线程安全性(Thread Safety):由于字符串常常被多个线程共享,如果字符串是可变的,那么在并发情况下可能会出现竞态条件和线程安全问题。通过将字符串设置为不可变,可以避免这些问题,从而简化并发编程。
-
字符串池(String Pool):Java 中的字符串常量池是 JVM 中的一块特殊内存区域,用于存储字符串常量。由于字符串是不可变的,因此可以安全地在字符串常量池中共享字符串对象,从而减少内存占用和提高性能。
-
优化(Optimization):由于字符串是不可变的,因此可以进行一些优化,如字符串的哈希码可以提前计算并缓存,字符串拼接时可以使用
StringBuilder
或StringBuffer
,而不必担心线程安全问题。
API 和 SPI 的区别
-
API(应用程序编程接口):
- API 是一组定义了软件组件之间交互的规范和约定,用于描述如何调用某个组件或模块提供的功能。
- API 提供了一种标准化的接口,使得不同的软件组件可以进行交互和通信,而无需了解其内部实现细节。
- API 定义了对外暴露的方法、参数、返回值等,是软件开发的基础。
-
SPI(服务提供者接口):
- SPI 是一种服务发现机制,用于在运行时动态加载并实现特定的接口或功能。
- SPI 允许在不修改代码的情况下扩展应用程序的功能,通过在类路径上查找并加载服务提供者的实现类。
- SPI 包含两部分:接口(Service Interface)和服务提供者(Service Provider)。接口定义了服务的规范,而服务提供者则提供了接口的具体实现。
主要区别在于,API 是一种固定的编程接口,用于描述组件之间的交互规范;而SPI 则是一种动态加载机制,用于在运行时动态加载并实现特定接口的功能。API 是面向开发者的,SPI 则更多地面向服务的扩展。