1、接口和抽象类的区别
接口是一种规范,它定义了一个类或对象应该具有的方法。它只能包含方法的签名而没有实现,类通过实现接口来遵循这些方法的规定。一个类可以实现多个接口。
抽象类是一个类,它具有抽象方法和非抽象方法。抽象方法是没有具体实现的方法,只有方法声明。非抽象方法具有具体实现。抽象类本身不能被实例化,需要通过继承来使用。
区别主要有以下几点:
- 接口只包含方法签名,没有具体实现;抽象类可以包含抽象方法和具体实现的非抽象方法。
- 类实现接口时,必须实现接口中定义的所有方法;类继承抽象类时,实现所有的抽象方法。
- 一个类可以同时实现多个接口,但只能继承一个抽象类。
- 接口可以被其他接口继承,形成接口的继承链;抽象类可以被其他类继承,形成类的继承链。
- 接口中的变量默认是常量,不能被修改;抽象类中的变量可以是任意类型的,并且可以被修改
接口 | 抽象类 | |
方法 | 抽象方法 | 既可以有抽象方法,也可以有普通方法 |
关键字修饰 | interface | abstract |
定义常量变量 | 只能定义静态常量 | 成员变量 |
子类方法 | 所有方法必须实现 | 实现所有的抽象方法 |
子类继承 | 多继承 | 单继承 |
构造方法 | 不能有构造方法 | 可以有构造方法 |
接口实现 | 只能继承接口,不能实现接口 | 可以实现接口,并且不实现接口中的方法 |
2、Java 中有哪些数据结构?用过 HashMap 吗,说一下 HashMap 底层实现。
Java 中有很多数据结构,其中包括数组、链表、栈、队列、堆、树、图等。常见的 Java 数据结构类有:
- ArrayList / LinkedList:动态数组 / 链表
- HashSet / TreeSet:哈希集合 / 树集合
- HashMap / TreeMap:哈希映射 / 树映射
- ArrayDeque / LinkedList:双端队列 / 双向链表
- PriorityQueue:优先队列
至于 HashMap 的底层实现,它是一种使用数组和链表来存储键值对的数据结构。具体来说,HashMap 内部维护了一个数组,每个数组元素都是一个链表的头节点,链表节点上存储了键值对。
当向 HashMap 中添加一个键值对时,首先会根据键的哈希值计算出在数组中的位置,在该位置的链表上查找是否已经存在相同的键,如果已经存在则更新该键对应的值,否则将新的键值对插入到链表的头部。
3、Java 中用的是值传递还是引用传递?
在Java中,参数传递方式是值传递(pass by value)。
当你将一个变量作为参数传递给一个方法时,实际上是将该变量的值进行拷贝传递给方法的参数,而不是传递变量本身的引用。无论是基本数据类型还是对象类型,都是按值传递的方式进行传递。
对于基本数据类型(如int、float、boolean等),传递的是该变量的值的副本,对方法内部的操作不会影响原始变量的值。
对于对象类型,传递的是该对象的引用的副本。这意味着方法内部可以通过引用修改对象的状态,但如果方法内部重新分配了对象的引用,原始引用不会受到影响。
让我们通过一个例子来说明:
public class Main {
public static void main(String[] args) {
int x = 5;
changeValue(x);
System.out.println(x); // 输出:5
StringBuilder sb = new StringBuilder("Hello");
changeReference(sb);
System.out.println(sb.toString()); // 输出:Hello World
}
public static void changeValue(int value) {
value = 10;
}
public static void changeReference(StringBuilder str) {
str.append(" World");
}
}
在上面的例子中,我们定义了两个方法`changeValue()`和`changeReference()`来改变参数的值。在第一个方法中,我们尝试修改传递的整数值,但这不会影响原始变量`x`的值。在第二个方法中,我们通过传递StringBuilder对象的引用,成功地修改了对象的内容。
因此,尽管Java中使用的是值传递,但对于对象类型,传递的是引用的副本,这使得我们可以在方法内部操作对象的状态。
4、面向过程和面向对象有什么区别?
面向过程是一种以过程为中心的编程思想,在处理某件事的时候,以正在进行什么为主要目标,分步骤完成目标。而面向对象的思想是将事物抽象为对象,赋予其属性和方法,通过每个对象执行自己的方法来完成目标。面向过程效率更高,而面向对象耦合低(易复用),扩展性强,易维护。
5、final、finally、finalize 的区别?
在Java中,final、finally和finalize是三个具有不同含义和用途的关键字。
1. final:final是一个修饰符,可应用于类、方法和变量。使用final修饰的类不能被继承,final修饰的方法不能被子类重写,final修饰的变量表示常量,其值一旦赋值后就不能再改变。
2. finally:finally是一个关键字,用于定义在try-catch语句块中的代码块,无论是否发生异常,finally代码块中的语句都会被执行。finally通常用于释放资源、关闭连接等必须执行的操作。
try {
// 可能会抛出异常的代码
} catch (Exception e) {
// 异常处理
} finally {
// 无论是否发生异常,都会执行的代码
}
3. finalize:finalize是Object类中的一个方法,用于在垃圾回收器回收对象之前执行一些清理操作。这个方法在对象被销毁之前会被自动调用,但并不能保证一定会被执行,因为垃圾回收的时机是由JVM决定的。一般来说,我们不建议过多地依赖finalize方法进行资源释放,而是应该使用finally块或其他方式手动释放资源。
总结一下:
- final是一个修饰符,用于修饰类、方法和变量。
- finally是一个关键字,用于定义在try-catch语句块中无论是否发生异常都会执行的代码块。
- finalize是Object类中的一个方法,用于在对象被垃圾回收之前执行一些清理操作。
6、什么是序列化?什么是反序列化?
序列化(Serialization)是将对象转换为字节流的过程,使得对象可以在网络上传输或者持久化到硬盘上。反序列化(Deserialization)则是将字节流重新转换为对象的过程,使得对象可以被重新使用。
在Java中,通过实现`Serializable`接口来实现序列化和反序列化。`Serializable`接口是一个标记接口,没有任何方法定义,只是起到一个标记的作用。当一个类实现了`Serializable`接口之后,表示该类的对象可以被序列化。
要进行序列化和反序列化,可以利用`ObjectOutputStream`和`ObjectInputStream`类。`ObjectOutputStream`用于将对象写入输出流,而`ObjectInputStream`用于从输入流中读取对象。
下面是一个简单的示例:
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
// 序列化对象
try {
// 创建一个对象
Student student = new Student("John", 20);
// 创建一个输出流
FileOutputStream fileOut = new FileOutputStream("student.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// 写入对象
out.writeObject(student);
// 关闭流
out.close();
fileOut.close();
System.out.println("对象已被序列化并保存到文件中。");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化对象
try {
// 创建一个输入流
FileInputStream fileIn = new FileInputStream("student.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取对象
Student student = (Student) in.readObject();
// 关闭流
in.close();
fileIn.close();
System.out.println("从文件中读取到的对象是:" + student.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Student implements Serializable {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
在上面的示例中,我们创建了一个`Student`类,并实现了`Serializable`接口。通过创建一个`Student`对象并将其写入输出流,我们可以将对象序列化并保存到文件中。然后,我们再从文件中读取字节流,并通过输入流进行反序列化,最终得到了原始的`Student`对象。
需要注意的是,被序列化的类必须实现`Serializable`接口,否则在序列化或反序列化时会抛出`NotSerializableException`异常。同时,静态变量和transient修饰的变量不会被序列化。
输出就是将对象转换为字节码,输入就是将字节码读取为对象
7、为什么 Java 中 String 是不可变类?
Java 中的 String 是不可变类,这是因为 Java 设计者希望字符串具有不可变性带来的一些好处,包括安全性、线程安全性和性能优化等方面的考量。
以下是一些原因:
1. **安全性**:字符串常常用作哈希表的键值,如果字符串是可变的,那么在作为键值的时候,如果发生了改变,可能导致在哈希表中无法正确找到对应的值。通过将字符串设为不可变,可以避免这种情况的发生。
2. **线程安全**:由于字符串是不可变的,多个线程可以同时访问和共享字符串对象,而不需要额外的同步措施,这提高了程序的并发性能。
3. **缓存优化**:由于字符串是不可变的,可以在编译期间进行优化,比如字符串常量池的使用,可以缓存字符串对象以便重用,减少内存占用和对象创建的开销。
4. **安全性**:如果字符串是可变的,那么它可以被修改,这可能会导致不可预测的结果。通过使字符串不可变,可以避免在程序的各个部分产生意外的影响。
总之,将字符串设计为不可变类可以带来更好的安全性和性能优势。因此,Java 中的 String 类被设计为不可变的。
8、线程和进程的区别
进程是程序的运行实例,拥有独立的内存空间和资源,而线程是进程内的执行单元,共享进程的资源,用于实现并发处理。
进程 | 线程 | |
定义 | 操作系统进行资源分配和调度的基本单位 | 操作系统能够进行运算调度的最小单位 |
从属关系 | 运行程序的实例 | 进程的一个执行流 |
资源共享 | 进程间不能共享资源 | 线程间可以共享资源 |
上下文切换 | 切换速度慢 | 切换速度快 |
操纵方 | 操作系统 | 编程人员 |
9、Java 如何实现线程安全
Java 中实现线程安全的方式主要包括以下几种:
1. 使用同步方法:通过在方法前加上 synchronized 关键字,确保同一时刻只有一个线程可以访问该方法,从而保证方法内部的操作是线程安全的。
```java
public synchronized void synchronizedMethod() {
// 线程安全的操作
}
```
2. 使用同步代码块:通过在代码块内使用 synchronized 关键字对对象进行加锁,控制多个线程对共享资源的访问。
```java
public void synchronizedBlock() {
synchronized (this) {
// 线程安全的操作
}
}
```
3. 使用 Lock 接口:通过 Lock 接口及其实现类(如 ReentrantLock)来手动控制线程的加锁和解锁操作,提供了更灵活的锁定机制。
```java
private Lock lock = new ReentrantLock();
public void lockExample() {
lock.lock();
try {
// 线程安全的操作
} finally {
lock.unlock();
}
}
```
4. 使用并发容器:Java 提供了一些线程安全的容器类,如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些容器内部实现了同步机制,可以安全地在多线程环境中使用。
```java
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
```
5. 使用 volatile 关键字:volatile 可以确保变量的可见性,即一个线程修改了变量的值,其他线程能立即看到最新的值,但并不能保证原子性操作,适用于简单的标识位和状态控制。
```java
private volatile boolean flag;
```
通过以上方式,可以在 Java 中实现线程安全,确保多个线程访问共享资源时不会出现数据竞争和不一致的情况,从而提高程序的并发性能和稳定性。
10、Java线程池
在 Java 中,主要有以下几种类型的线程池:
-
FixedThreadPool(固定大小线程池):
- 该线程池中线程数量固定,不会动态调整。
- 当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则任务会被放入队列等待。
- 适用于需要控制并发数的场景。
-
CachedThreadPool(缓存线程池):
- 该线程池可以根据需要创建新线程,但会在某个线程空闲一段时间后回收该线程。
- 如果线程池中有可用的线程,会重复使用;否则会创建新线程。
- 适用于执行很多短期异步任务的场景。
-
SingleThreadExecutor(单线程线程池):
- 该线程池只有一个工作线程,保证所有任务按顺序执行。
- 适用于需要顺序执行任务的场景。
-
ScheduledThreadPoolExecutor(定时任务线程池):
- 该线程池用于执行定时任务和周期性任务。
- 可以设定延迟时间和执行周期,定时执行任务。
10、Java 中有哪些锁
11、怎么理解乐观锁和悲观锁?
乐观锁和悲观锁是并发控制的两种不同策略,用于处理多线程环境下的数据访问冲突。
-
悲观锁:
- 悲观锁的思想是认为在并发环境下,总会有其他线程来竞争资源,并且会导致冲突。
- 因此,在使用悲观锁时,线程在访问共享资源之前会先将其加锁,确保其他线程无法同时访问该资源。
- 典型的悲观锁包括 synchronized 锁和 ReentrantLock 锁。
-
乐观锁:
- 乐观锁的思想是认为在并发环境下,冲突的概率较低,因此不进行加锁,而是采用一种乐观的方式进行操作。
- 在使用乐观锁时,线程在访问共享资源之前不会加锁,而是假设没有冲突并直接进行操作。
- 当实际执行操作时,通过某种机制(如版本号、时间戳等)检查是否发生了冲突,如果发生了冲突,则进行相应的处理(如重试、回滚等)。
- 典型的乐观锁包括 CAS(Compare and Swap)操作和版本号控制。
乐观锁适用于读操作较多、写操作较少的场景,可以提高并发性能。悲观锁适用于写操作较多、临界资源竞争激烈的场景,可以保证数据的一致性。
需要注意的是,乐观锁并不能完全解决并发冲突问题,因为在乐观锁的机制下,如果检测到冲突,需要进行相应的处理,可能会导致重试或回滚等操作,降低了程序的效率。
12、怎么理解自旋锁?为什么还会有自旋锁?
自旋锁是一种基于忙等待的锁机制,线程在获取锁时,如果发现锁已被其他线程占用,它并不会阻塞等待,而是会一直循环检测锁是否被释放,直到获取到锁为止。
自旋锁的设计初衷是为了解决线程阻塞和唤醒所带来的性能开销。当线程需要获取一个短时间内即将被释放的锁时,使用自旋锁可以避免线程切换和上下文切换的开销,从而提高程序的性能。
自旋锁的实现方式通常使用了底层的原子操作,如 CAS(Compare and Swap)操作,它可以实现无锁并发控制。线程在自旋期间会不断地尝试CAS操作去获取锁,如果成功则退出自旋,否则继续自旋。
然而,自旋锁也存在一些问题和适用场景的限制:
- 自旋锁适用于锁的占用时间非常短暂的场景,如果锁的占用时间较长,自旋的线程会一直占用CPU资源,降低系统的整体性能。
- 在多核处理器上,自旋锁可能导致线程在不同的CPU核心之间进行忙等待,对于共享缓存的一致性可能会带来额外开销。
- 自旋锁不适用于线程数较多、竞争激烈的场景,这样会导致大量线程进行自旋,浪费大量的CPU资源。
因此,自旋锁通常在以下情况下使用较为合适:
- 对共享资源的锁竞争时间非常短暂,几乎没有等待时间。
- 并发竞争较轻,系统负载较低。
- 多核处理器上的共享数据访问。
13、String常见方法
String 是 Java 中最基础的类之一,它提供了很多常用的方法和操作字符串的功能。
charAt(int index)
:获取指定位置的字符。length()
:返回字符串的长度。substring(int beginIndex)
:返回从指定位置开始到末尾的子字符串。substring(int beginIndex, int endIndex)
:返回指定范围内的子字符串。equals(Object anObject)
:比较字符串是否相等。equalsIgnoreCase(String anotherString)
:忽略大小写比较字符串是否相等。indexOf(int ch)
:返回指定字符在字符串中第一次出现的位置。indexOf(String str)
:返回指定字符串在字符串中第一次出现的位置。lastIndexOf(int ch)
:返回指定字符在字符串中最后一次出现的位置。lastIndexOf(String str)
:返回指定字符串在字符串中最后一次出现的位置。replace(char oldChar, char newChar)
:返回一个新字符串,其中所有旧字符都被替换为新字符。replaceAll(String regex, String replacement)
:使用正则表达式替换字符串中的所有匹配项。startsWith(String prefix)
:测试此字符串是否以指定的前缀开始。endsWith(String suffix)
:测试此字符串是否以指定的后缀结束。toLowerCase()
:将字符串转换为小写。toUpperCase()
:将字符串转换为大写。trim()
:返回去掉字符串前后空格的新字符串。
14、synchronized 关键字
`synchronized` 是 Java 中用于实现线程同步的关键字。它可以用于方法或代码块上,用来控制多个线程对共享资源的访问。
1. **同步方法**:在方法声明中使用 `synchronized` 关键字,使得该方法成为一个同步方法。同一时间只有一个线程可以进入该方法,其他线程需要等待。
```java
public synchronized void synchronizedMethod() {
// 同步代码块
// ...
}
```
2. **同步代码块**:使用 `synchronized` 关键字来控制代码块的同步,指定一个对象作为锁,并在执行代码块之前获取该锁。
```java
public void someMethod() {
// 其他非同步代码
synchronized (lockObject) {
// 需要同步的代码块
// ...
}
// 其他非同步代码
}
```
在上述示例中,`lockObject` 是一个任意的对象,通过该对象实现对代码块的同步控制。同一时间只有一个线程可以获取 `lockObject` 的锁并执行同步代码块。
`synchronized` 关键字提供了以下特性:
- 当一个线程进入同步方法或同步代码块时,它会尝试获取锁,如果锁没有被其他线程占用,那么该线程将获取到锁并继续执行代码。
- 如果锁已经被其他线程占用,那么线程将进入阻塞状态,等待获取锁。
- 当线程执行完同步方法或同步代码块后,会释放锁,其他等待的线程将有机会获取锁。
`synchronized` 关键字保证了线程对共享资源的安全访问,避免了竞态条件和数据不一致的问题。然而,使用 `synchronized` 可能会引起线程的阻塞和上下文切换,降低程序的性能。因此,在使用 `synchronized` 时需要注意合理控制同步的粒度,避免过度同步造成性能瓶颈。此外,Java 还提供了更高级的并发工具类,如 `Lock` 接口和 `ReentrantLock` 类,可以更灵活地实现线程同步。
15、什么是反射机制?为什么反射慢?
反射机制是指在运行时动态地获取类的信息并操作类的属性、方法和构造函数等的能力。
在Java中,可以使用多种方式来获取类对象(Class对象),主要包括以下几种方式:
1. **通过类名直接获取**:可以使用`.class`语法来获取类的Class对象。例如,要获取User类的Class对象,可以使用`User.class`。
Class<User> clazz = User.class;
2. **通过对象实例获取**:如果已经有一个对象实例,可以通过该对象调用`getClass()`方法来获取其所属类的Class对象。
User user = new User();
Class<? extends User> clazz = user.getClass();
3. **通过全限定类名获取**:可以使用`Class.forName()`方法根据类的全限定类名来获取Class对象。需要注意的是,`Class.forName()`方法会加载并初始化该类。
Class<?> clazz = Class.forName("com.example.User");
4. **通过ClassLoader获取**:可以通过ClassLoader的`loadClass()`方法来加载类并获取对应的Class对象。这种方式相对灵活,可以动态加载类。
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.User");
这些方式可以根据不同的需求和情况来获取类对象,反射机制使得在运行时可以动态地操作类和对象,为Java编程提供了更大的灵活性。