【年后找工作】Java八股必备知识 -- 基础篇

1、接口和抽象类的区别

接口是一种规范,它定义了一个类或对象应该具有的方法。它只能包含方法的签名而没有实现,类通过实现接口来遵循这些方法的规定。一个类可以实现多个接口。

抽象类是一个类,它具有抽象方法和非抽象方法。抽象方法是没有具体实现的方法,只有方法声明。非抽象方法具有具体实现。抽象类本身不能被实例化,需要通过继承来使用。

区别主要有以下几点:

  1. 接口只包含方法签名,没有具体实现;抽象类可以包含抽象方法和具体实现的非抽象方法。
  2. 类实现接口时,必须实现接口中定义的所有方法;类继承抽象类时,实现所有的抽象方法。
  3. 一个类可以同时实现多个接口,但只能继承一个抽象类。
  4. 接口可以被其他接口继承,形成接口的继承链;抽象类可以被其他类继承,形成类的继承链。
  5. 接口中的变量默认是常量,不能被修改;抽象类中的变量可以是任意类型的,并且可以被修改
接口抽象类
方法抽象方法既可以有抽象方法,也可以有普通方法
关键字修饰interfaceabstract
定义常量变量只能定义静态常量成员变量
子类方法所有方法必须实现实现所有的抽象方法
子类继承多继承单继承
构造方法不能有构造方法可以有构造方法
接口实现只能继承接口,不能实现接口可以实现接口,并且不实现接口中的方法

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 中,主要有以下几种类型的线程池:

  1. FixedThreadPool(固定大小线程池)

    • 该线程池中线程数量固定,不会动态调整。
    • 当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则任务会被放入队列等待。
    • 适用于需要控制并发数的场景。
  2. CachedThreadPool(缓存线程池)

    • 该线程池可以根据需要创建新线程,但会在某个线程空闲一段时间后回收该线程。
    • 如果线程池中有可用的线程,会重复使用;否则会创建新线程。
    • 适用于执行很多短期异步任务的场景。
  3. SingleThreadExecutor(单线程线程池)

    • 该线程池只有一个工作线程,保证所有任务按顺序执行。
    • 适用于需要顺序执行任务的场景。
  4. ScheduledThreadPoolExecutor(定时任务线程池)

    • 该线程池用于执行定时任务和周期性任务。
    • 可以设定延迟时间和执行周期,定时执行任务。

10、Java 中有哪些锁

11、怎么理解乐观锁和悲观锁?

乐观锁和悲观锁是并发控制的两种不同策略,用于处理多线程环境下的数据访问冲突。

  • 悲观锁

    • 悲观锁的思想是认为在并发环境下,总会有其他线程来竞争资源,并且会导致冲突。
    • 因此,在使用悲观锁时,线程在访问共享资源之前会先将其加锁,确保其他线程无法同时访问该资源。
    • 典型的悲观锁包括 synchronized 锁和 ReentrantLock 锁。
  • 乐观锁

    • 乐观锁的思想是认为在并发环境下,冲突的概率较低,因此不进行加锁,而是采用一种乐观的方式进行操作。
    • 在使用乐观锁时,线程在访问共享资源之前不会加锁,而是假设没有冲突并直接进行操作。
    • 当实际执行操作时,通过某种机制(如版本号、时间戳等)检查是否发生了冲突,如果发生了冲突,则进行相应的处理(如重试、回滚等)。
    • 典型的乐观锁包括 CAS(Compare and Swap)操作和版本号控制。

乐观锁适用于读操作较多、写操作较少的场景,可以提高并发性能。悲观锁适用于写操作较多、临界资源竞争激烈的场景,可以保证数据的一致性。

需要注意的是,乐观锁并不能完全解决并发冲突问题,因为在乐观锁的机制下,如果检测到冲突,需要进行相应的处理,可能会导致重试或回滚等操作,降低了程序的效率。

12、怎么理解自旋锁?为什么还会有自旋锁?

自旋锁是一种基于忙等待的锁机制,线程在获取锁时,如果发现锁已被其他线程占用,它并不会阻塞等待,而是会一直循环检测锁是否被释放,直到获取到锁为止。

自旋锁的设计初衷是为了解决线程阻塞和唤醒所带来的性能开销。当线程需要获取一个短时间内即将被释放的锁时,使用自旋锁可以避免线程切换和上下文切换的开销,从而提高程序的性能。

自旋锁的实现方式通常使用了底层的原子操作,如 CAS(Compare and Swap)操作,它可以实现无锁并发控制。线程在自旋期间会不断地尝试CAS操作去获取锁,如果成功则退出自旋,否则继续自旋。

然而,自旋锁也存在一些问题和适用场景的限制:

  1. 自旋锁适用于锁的占用时间非常短暂的场景,如果锁的占用时间较长,自旋的线程会一直占用CPU资源,降低系统的整体性能。
  2. 在多核处理器上,自旋锁可能导致线程在不同的CPU核心之间进行忙等待,对于共享缓存的一致性可能会带来额外开销。
  3. 自旋锁不适用于线程数较多、竞争激烈的场景,这样会导致大量线程进行自旋,浪费大量的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编程提供了更大的灵活性。

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值