Java 面试
String、StringBuffer、StringBuilder 区别
String是不可变类,一旦创建无法修改,这是因为在String内部使用 final 来修饰真正存储数据的字符数组,当对字符吃创建进行修改的时候,比如字符串拼接,实际上使用创建一个新的字符串对象,原来的对象是保持不变的。字符串是存储在字符串常量池中的。
StringBuilder 和 StringBuffer 都是可变类,底层是一个可变字符数组来存储数据的,所以在调用相关方法修改字符串的时候,是不会创建新的字符串的。
StringBuilder 和 StringBuffer的区别:
- 使用 StringBuilder 是非线程安全的,而StringBuffer 在底层使用 synchronized 修饰的
- 由于StringBuffer 使用 synchronized 修饰,会带来一定的性能开销,效率不如 StringBuilder
- 在多线程的环境下使用 StringBuffer , 在非多线程环境下使用 StringBuilder
注意:String 也是线程安全的 , 因为 String本身就是不可变的。
Java 中多态的实现方式有哪些
多态定义:允许不同类对象对象同一种消息做出不同的响应。
方法重载(静态多态,编译阶段)
方法重载:在同一个类中,定义多个方法名相同,但是参数列表不同(参数个数、类型或者顺序不同)的方法。在编译阶段就确定了。(根据方法传入的参数类型不同,做出不一样的处理)
方法重写(动态多态,运行阶段)
方法重写:发生在子类和父类之间,子类重新定义父类中已有的方法。但是使用父类来接收子类new出来的对象,并且使用父类的变量名调用重写后的方法,实际调用的子类重写的方法。
接口实现 (动态多态,运行阶段)
接口实现:不同的实现类重写同一个方法,可以方法的逻辑可以不同。
Java 中如何处理异常
try-catch-finally
try-catch 捕获异常并且进行处理,当 try 中代码出现异常就会走到 catch 中,如果没有就不会执行 catch 中代码,finally 中的代码不论代码使用出现异常都会执行。
throw
throw 手动抛出异常,一般在代码逻辑中,当一些条件是就抛出异常来通知调用者。
throws
throws:在方法声明中声明该方法可能会抛出那些异常。在该方法内部不进行处理,有调用处进行处理或者接着抛出。
集合框架
集合主要分为单例集合和双列集合
单列集合:只有 value
双列集合:key-value 结构
单列集合
双列集合
Java 中线程的创建方式
线程的创建主要有三种方式:
- 继承 Thread 类(s red)
- 特点:直接创建该类的子类,并且重写 run 方法,run 方法中代码就是该线程需要做的事情。在通过该子类的对象调用 start 方法就可以创建线程并且运行了。
- 缺点:由于 Java只支持单继承,一个类继承了 Thread 就不能继承其他了,限制了类的拓展性。
- 无返回值。
- 实现 Runnable 接口 (卵了煲)
- 特点:创建类实现该接口并且重写 run 方法,这种方式不会占用类的继承,增加了该类的拓展性。
- 缺点:需要借助 Thread 来管理线程。
- 无返回值
- 实现 Callable 接口(K了煲)
- 特点:创建类实现该接口并且重写 call 方法 ,这种方式不会占用类的继承,增加了该类的拓展性。
- 缺点:创建和使用的时候比较复杂,需要 Thread 和 futureTask 类配合。
- 有返回值。
- 使用线程池(常用)
- 特点:可实现线程复用,不用频繁创建线程和销毁线程
- 缺点:实现比较复杂。
Java 中的反射机制
Java 中的反射机制是指在程序运行时,能够动态地获取一个类的所有信息,包括属性、方法、构造方法等,并且可以对这些成员进行操作。
使用反射的步骤: 1. 先获取 class 对象 ,2. 获取成员
获取 class 对象的方式有
Class.forName("全类名")
- 比较灵活,可以通过读取配置文件中内容来进行反射
类.class
- 类只要类存在就可以使用这个方式
对象.getClass
- 需要对象已经存在才能使用这个方式获取。
获取成员方式
获取属性
若要获取公共属性,可以使用 Class
对象的 getField(String name)
方法。但如果要获取所有属性(包括私有属性),则需要使用 getDeclaredField(String name)
方法。可通过 get和set 方法来获取属性值和修改属性值。
获取构造方法
getConstructors()
方法用于获取所有公共构造方法,而 getDeclaredConstructors()
方法可以获取所有构造方法(包括私有构造方法)。获取构造方法对象 Constructor
后,可通过 newInstance
方法创建对象。
获取方法
使用 Class
对象的 getMethod(String name, Class<?>... parameterTypes)
方法获取指定的公共方法,getDeclaredMethod(String name, Class<?>... parameterTypes)
方法用于获取所有方法(包括私有方法)。获取 Method
对象后,通过 invoke
方法调用方法。
Java 中的序列化和反序列化
序列化和反序列化:主要用于数据持久化和网络上数据传输。
序列化:(使用 Java 默认的序列化)器将 Java对象转换字节串。需要序列化的类应该实现 Serializable
接口(序列化接口)并且设置序列化版本号,注意这个只是一个标识,用于告知虚拟机该类的对象可以进行序列化。
反序列化:(使用 Java 默认的序列化)将字节串转换为 Java 对象。
序列化和反序列化配合使用可以不同的环境进行数据传输。
Java 中的泛型是什么
泛型:不确定的数据类型。
泛型好处:类型检查和代码复用。
泛型使用方式:
- 泛型类:在该类中可以使用泛型
- 泛型方法:在该方法可以使用泛型
- 泛型接口:该接口的实现类(进行泛型传递)和接口方法都可以使用。
泛型限定范围:(以 User 类来做实例)
- 限定泛型必须是 User 的后代类(包括自己), < ? extends User>
- 限定泛型必须是 User 的祖先类 (包括自己) , < ? super User>
Java 中的泛型是伪泛型,编译成 Class 泛型会替换成 Object 类。
Java 中的注解是什么
Java 中注解是一种元数据,他可以为代码在编译、运行等阶段提供一些额外的数据,开发者可以通过这数据,来实现特定的功能:比如代码检查、代码生成等。
注解的特点:以 @
+ 标识符 ,常见的注解有 @Override
等注解。
元注解:注解的注解。
注解实例:比如 @OVerride
就可以用标识一个方法是重写方法,如果方法不是重写方法使用 @Override
标识出现编译错误。
Java 中内存模型
Java中内存模型分为五部分:
- 栈
- 堆
- 方法区
- 本地方法栈
- 程序计数器
Java 中的垃圾回收机制(GC机制)
GC 机制:主要负责回收堆和方法区中不在使用的内存空间。
判断一个对象是否不再使用的方法:
- 引用计数法:每当有一个地方引用该对象,就会+1 , 每当有个一个地方不再引用该对象,就会 -1 。(可能出现,两个对象相互引用导致引用循环,这样对象就永远都不会被回收了。)
- 可达性分析算法:一个 "GC Roots" 对象为开始,往下找如果没有找到(不可达)该对象就可以被回收了,一般栈中引用对象、方法区中静态属性引用的对象,方法区中常量引用的对象等。
垃圾回收算法
1.标记清除算法
将存活的对象进行标记,然后清理掉未被标记的对象。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2. 标记整理算法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
3. 复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。这种算法适用于对象存活率较低的场景,新生代经常采用这种算法。
线程和进程的区别?
线程:cpu 调用的基础单元
进程:一个进程中包含多个线程,可以理解为进程是容器,里面装着线程。
并行和并发有什么区别
并行:同一个时间同时执行多个任务,多个cpu 同时执行多个线程。
并发:同一个时间需要做多个任务, 一个 cpu 轮流执行多个线程。
线程有哪些状态
新建:
- 当一个线程对象被创建出来,但是还没有调用 start 方法的时候就是新建状态。
可运行:
- 调用的 start 方法,但是没有获取的执行资格。
终结 :
- 线程内代码执行的完毕,就冲运行状态转换为终结状态
阻塞:
- 当线程获取锁失败后,就会进入阻塞状态。
- 当持有锁的线程释放了锁是,就会按照一定规则唤醒被阻塞队列中的阻塞线程,唤醒的线程重新回到了 可运行状态(没有获取 cpu 的执行权 )
等待:
- 当获取锁成功后,但是由于条件不满足,调用了 wait 方法,等待唤醒
- 当其他线程调用 notify() 或者 notifyAll() 方法,来唤醒执行了 wait 的线程
- 示例:厨师和客人,厨师做饭,客人吃饭
- 厨师没有做好饭,客人就等待厨师做好饭 (客人等待),厨师做好饭了就会告知客人可以吃饭了 (唤醒客人)。
睡眠:
- 线程调用 sleep 方法,线程就进入等待状态,等待过了指定时间,就回到了 可运行状态。
线程池
创建线程池的参数
在线程池中一共有7个核心参数:
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
void test() {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5, // 核心线程数量
10, // 最大线程数量 (临时线程数 = 最大线程 - 核心线程数 )
1, // 临时线程的存活时间
TimeUnit.MINUTES, // 临时线程的存活时间的单位
new ArrayBlockingQueue<>(10), // 线程池满了,会创建一个队列来保留这些超出的任务
// r -> new Thread(r , "myThread" ), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 线程池的拒绝策略,当所有线程都在执行任务,并且等待队列中也已经满了,就会触发拒绝策略
);
// 执行线程任务
poolExecutor.execute(() -> System.out.println("hello "));
}