前阿里P7分享经典《面试必备知识》——Java基础101问
前言
本期分享的是与Java基础相关的面试题,希望对大家有帮助。
整理不易,希望大家能够点赞、收藏、转发一下!
目录如下,请善用CTRL+F查找
1. JDK,JRE,JVM分别是什么?
2. Java的ME、SE、EE分别是什么?
- java ME主要用于移动端和嵌入式设备开发
- java SE主要用于桌面级应用开发
- java EE主要用于企业级应用开发
3. Java有几种基本类型,分别是什么?不同类型占用的大小?缓存区间?
- byte/8
- char/16
- short/16
- int/32
- float/32
- long/64
- double/64
- boolean/~
- 《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。这其实是运算效率和存储空间之间的博弈,两者都非常的重要
4. 基本类型对应的包装类型?
- 包装类型会对高频区间的数据进行缓存
- 缓存数据直接由IntergetCache.cache产生,复用已有对象,在这个区间的对象可以直接使用==判断
- 注意需要以 Integer var = 1 这种形式生成对象才可以
5. 什么是序列化?
-
序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。
-
对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象
-
如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(serializable)。
-
目的:将对象序列化成字节序列,方便存储和运输
6. Java原生序列化介绍一下?
-
需要类实现Serializable接口
-
该接口没有任何方法,只是起标识作用
-
实现接口的同时建议设置serialVersionUID字段,序列化与反序列过程会根据该字段判断是否是同一个类
-
没有设置的话,编译器会根据类的内部实现,包括类名、接口名、方法和属性等来自动生成
-
如果类的源码发生了修改,那么重新编译生成的UID就会不同,导致反序列化失败
-
因此:
- 如果是兼容升级,请不要修改UID,避免反序列化失败
- 如果是不兼容升级,请修改UID,避免反序列化混乱
-
注意:原生序列化机制在反序列化的过程中不会调用类的无参构造器方法,而是调用native方法将成员变量赋值为对应类型的初始值
-
基于性能与兼容性考量,不推荐使用原生序列化
-
使用案例:
-
一般配合对象输出流和对象输入流使用
public class RpcClient { private static final Logger logger = LoggerFactory.getLogger(RpcClient.class); public Object sendRequest(RpcRequest rpcRequest, String host, int port) { try (Socket socket = new Socket(host, port)) { // 核心部分:将对象通过对象输出流或者对象输入流写出或写入的时候 // 会使用JDK的序列化机制实现对象的序列化 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); objectOutputStream.writeObject(rpcRequest); objectOutputStream.flush(); return objectInputStream.readObject(); } catch (IOException | ClassNotFoundException e) { logger.error("调用时有错误发生:", e); return null; } } }
7. Hessian序列化介绍一下?
8. JSON序列化介绍一下?
- 注意:
- 对于敏感或者不需要传输的数据,可以加上transient关键字,避免将其转化为序列化的二进制流
- 如果一定要传输敏感信息,则可以使用对称和非对称加密方式独立传输
- 对传入数据还得再进行校验或者权限控制
9. java序列化的框架有哪些?
- JDKSerialize
- FastjsonSerialize
- FSTSerialize
- GsonSerialize
- JacksonSerialize
- 功能全面,提供多种模式的json解析方式,“对象绑定”使用方便,利用注解包能为我们开发提供很多便利。
- 性能较高,“流模式”的解析效率超过绝大多数类似的json包。
10. 介绍一下字节码执行模式?
11. 封装、继承、多态的含义?
- 封装:
- 隐藏对象的属性和实现细节,对外提供公共的访问方式,以防止数据的随意访问和修改。
- 继承:
- 通过扩展一个已有的类,并继承该类的属性和行为,来创建一个新的类。
- 多态:
- 对于面向对象而言,多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
- 被调用的方法在编译期无法确定,需要等到程序运行的时候才能确定
- 三个条件:
- 满足继承关系
- 父类引用变量指向子类对象
- 子类重写父类的方法
12. java反射可以获得类的什么?
- 可以获得类的构造函数、字段、方法、父类等类的相关信息。
13. 反射获取类对象的方法有哪几种?
- 通过类的实例对象的getClass()方法
- 通过类名.class来获取,Person.class
- 通过Class.forName(),利用类的全限定类名来进行获取
14. 接口和抽象类的异同?
- 抽象类用abstract修饰,除了不能实例化抽象类外,与普通类的区别不太,但是接口与普通类是完全不同的类型
- 抽象类可以有普通的方法,接口是完全的抽象,没有方法的实现(public abstract)(JDK1.8开始可以有默认的实现)
- 抽象类可以有构造器,接口不能有
- 对于抽象类的实现类来说,可以通过super()调用父类抽象类定义的构造器方法
- 抽象类的方法可以有public、protected和default这些修饰符,接口方法只能是public
- 抽象类的变量是普通变量,接口中的变量只能是公共静态常量(public static final)
-
抽象类被继承时,体现的是 is-a 关系,接口被实现的时候体现的是 can-do 关系
-
接口的默认实现方法:
- 默认方法可以在接口中提供实现,但是默认方法只能在实现类或者通过实现类对象来调用
- 当多个父接口中存在相同的默认方法时,子类中以就近原则继承
- 抽象类是对类的抽象,是一种模板设计
- 需要定义子类的行为,又要为子类提供通用的功能
- 接口是对行为的抽象,是一种行为规范
15. 讲一下你对string类的理解?
-
string类是被final修饰的类,没有子类,不能被继承
-
string底层声明了一个名为value的字符数组,这个数组被final修饰,因此string对象是不可变的。
-
string可以通过直接赋值创建:
- 该方法先检查字符串常量池是否有该值的字符串对象,如果没有存在,那么就会在字符串常量池中创建这个字符串对象;如果存在,直接返回该字符串的内存地址值。
-
通过new关键字进行创建:
- 该方法首先会在堆上创建一个对象,然后检查常量池中是否有该字符串对象,有的话就返回该字符串的引用给堆上的对象,没有的话就创建一个字符串对象。最终返回的是堆上对象的地址值。
-
不同的字符串有可能共享同一个char数组
16. 字符串常量池的作用?
- 本质上是一个map,用来存放字符串的引用
- 当使用new来创建字符串的时候
- String s = new String("xyz");创建几个String实例?
- 首先是字面量"xyz"对应的字符串对象,这个对象存在于全局共享的字符串常量池中
- 如果该对象没有存在于常量池中的话,那么首先需要创建它
- 常量池只存放引用,不是实例对象
- 接下来是通过new关键字创建的内容与"xyz"相同的实例
- 实例对象在堆中,引用在栈上
17. 为什么string被设计为不可变?
- 方便实现字符串常量池
- 使用字符串常量池能够节省大量的内存空间
- 字符串不可变,因此hashcode只需要计算一遍缓存下来就可以了
- string不可变,因此作为hashmap的key非常合适,而且非常安全
- 天生的线程安全
18. 不可变对象是什么意思?
-
一个对象,在创建完毕后,不能再改变它的状态,即不能改变对象内的成员变量
- 基本类型的值不能变
- 引用类型的变量不能指向其它对象
- 引用类型指向的对象也不能变
- 除了构造函数以外,其它函数不能改变任何成员变量
- 任何使成员变量获得新值的函数都应该使新值保存在新的对象,保持原来对象不变
-
String作为不可变对象,其字符数组被private和final修饰,一般情况下通过外界是无法对其进行修改的
-
但是,可以通过反射强制修改数组的内容
19. String,Stringbuffer,StringBuilder的区别?
-
String类是一个不可变的类,一旦创建就不可以修改。
-
String是final类,不能被继承
-
String实现了equals()方法和hashCode()方法
-
继承自AbstractStringBuilder,是可变类。
-
StringBuffer是线程安全的
-
可以通过append方法动态构造数据。
-
继承自AbstractStringBuilder,是可变类。
-
StringBuilder是非线性安全的。
-
执行效率比StringBuffer高
20. 常见的java异常介绍几种?
- NullPointerException:空指针异常
- OutOfMemoryError :内存溢出错误(不是异常)
- ClassNotFoundException:指定类不存在
- NumberFormatException:字符串转换为数字异常
- IndexOutOfBoundsException:数组下标越界异常
- IllegalArgumentException:方法的参数错误
- ClassCastException:数据类型转换异常
- FileNotFoundException:文件未找到异常
21. 怎么查看java虚拟机内存占用?
- jmap(linux):打印出某个java进程(使用pid)内存内的所有'对象'的情况(如:产生那些对象,及其数量)。
- jstat:一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
- jps:与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。
22. JAVA IO模型有哪几种?
- JAVA五种IO模型
- IO多路复用?我所理解的IO模式
- IO过程一般包括两个阶段:
- 等待数据准备
- 将数组从内核拷贝到进程(从内核态到用户态)
- 阻塞IO(blocking IO):
- 在上述的两个状态中线程被阻塞了
- 非阻塞IO(non-blocking IO):
- 在第一阶段,用户进程会不断轮询数据是否准备好,如果准备好则进行第二阶段,第二阶段依然总体是阻塞的
- IO多路复用(IO Multiplxing):
- 事件驱动IO
- 底层用的是 select/poll/epoll等调用,优点是单个线程可以监控多个网路IO。
- 利用了新的select系统调用,让内核完成了轮询的操作
- 而在阻塞IO模型中,则必须使用多线程才能达到同样的效果。注意:这不意味着多路服用处理单个连接能处理的更快,只是单个线程能处理更多的连接
- 信号驱动(signal IO):
- 这个场景中,用户进程会通知内核,在数据准备好后要发个信号通知用户进程;用户进程在收到信号后发起系统调用等待内核将数据拷贝到用户线程。在第二阶段仍是阻塞的。
- 异步IO(asynchronous IO):
- 这个和信号驱动类似,不同的是直到数据拷贝到用户进程后才会发信号通知用户进程。整个过程不会阻塞用户线程。
23. 同步、异步、阻塞、非阻塞分别是什么意思?异同有哪些?
- 同步和异步关注的是消息通信机制
- 同步:
- 发出一个调用时,等待直到,调用结果的完成后返回。
- 异步:
- 发送一个调用后,立刻返回,不需要等待调用结果。通过通知机制或者回调函数通知。
- 同步:
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞:
- 发出调用后,当前线程被挂起,直到结果返回;
- 非阻塞:
- 发出调用后,不管 IO 操作是否结束,直接返回,当前线程继续保持运行状态。
- 阻塞:
- 阻塞/非阻塞:
- 描述的是调用者调用方法后的状态,比如:线程A调用了B方法,A线程处于阻塞状态。
- 同步/异步:
- 描述的方法跟调用者间通信的方式,如果不需要调用者主动等待,调用者调用后立即返回,然后方法本身通过回调,消息通知等方式通知调用者结果,就是异步的。如果调用方法后一直需要调用者一直等待方法返回结果,那么就是同步的
24. io多路复用的原理和实现?
- 多路复用通过linux的select\poll\epoll模型实现的
- IO 多路复用通过把多个 IO 阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下,可以同时处理多个 client 请求
25. StringUtils中 isNotEmpty 和isNotBlank的区别?【java字符串判空】
- isNotEmpty(str)等价于 str != null && str.length > 0
- isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length > 0
26. Java8新特性有哪些?
26.1 Lambda表达式使用方法?
26.2 流式编程如何使用?flatMap和map的区别?parallelStream是什么?底层是怎么实现的?forkjoin的原理?
-
flatMap和map的主要区别在于后者会将输入对象映射称为一个新的大集合
-
流式查找匹配字符串
Optional<a> aOptional = alist.stream().filter(e -> e.getId().equals(tar)).findFirst(); return gradeDefinitionOptional.isPresent() ? gradeDefinitionOptional.get() : null;
-
并行流 : parallelStream
-
底层使用forkjoin框架,简单理解就是多线程异步任务的一种实现
-
通过Java虚拟机参数:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N 设置worker的数目
-
问题
- 线程安全:由于并行流使用多线程,则一切线程安全问题都应该是需要考虑的问题,如:资源竞争、死锁、事务、可见性等等。
- 线程消费:在虚拟机启动时,我们指定了worker线程的数量,整个程序的生命周期都将使用这些工作线程;这必然存在任务生产和消费的问题,如果某个生产者生产了许多重量级的任务(耗时很长),那么其他任务毫无疑问将会没有工作线程可用;更可怕的事情是这些工作线程正在进行IO阻塞
-
小结:
串行流:适合存在线程安全问题、阻塞任务、重量级任务,以及需要使用同一事务的逻辑。
并行流:适合没有线程安全问题、较单纯的数据处理任务。
-
forkjoin pool的相关原理:
-
原理简述
- 它本身应该是对ThreadPoolExecutor的一个补充
- 底层会对大任务进行拆分,只有当任务比较小的时候才会进行处理,利用的是分治的思想
- 拆分任务是通过fork和join来完成的
- 每个线程会关联一个任务队列,每次调用fork的时候都会往队列中放入一个任务
- 调用join的时候,不会阻塞线程,此时线程会去队列中获取另外一个任务执行
- 如果自己队列中的任务执行完毕后,还会去偷其它线程的任务来执行
- 任务队列采用双向队列的设计,当前线程从尾部放入任务和获取任务,其它线程从头部偷窃任务,避免加锁
- 因此它可以使用较少的线程数目完成大量的子任务
-
与ThreadPoolExecutor的差异
- ThreadPoolExecutor中多个线程共享一个任务队列
- 线程无法向队列中添加任务,而且当前正在执行的任务由于某种原因无法执行的时候,线程会处于等待状态
- 因此无法使用分治的方法
-
适用场景
-
使用的地方
- parallel stream和CompletableFuture使用的默认线程池都是forkjoin pool
-
在底层中,会对任务进行拆分,递归处理
public class ForkJoinCalculator implements Calculator { private ForkJoinPool pool; //执行任务RecursiveTask:有返回值 RecursiveAction:无返回值 private static class SumTask extends RecursiveTask<Long> { private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) { this.numbers = numbers; this.from = from; this.to = to; } //此方法为ForkJoin的核心方法:对任务进行拆分 拆分的好坏决定了效率的高低 @Override protected Long compute() { // 当需要计算的数字个数小于6时,直接采用for loop方式计算结果 if (to - from < 6) { long total = 0; for (int i = from; i <= to; i++) { total += numbers[i]; } return total; } else { // 否则,把任务一分为二,递归拆分(注意此处有递归)到底拆分成多少分 需要根据具体情况而定 int middle = (from + to) / 2; SumTask taskLeft = new SumTask(numbers, from, middle); SumTask taskRight = new SumTask(numbers, middle + 1, to); taskLeft.fork(); taskRight.fork(); return taskLeft.join() + taskRight.join(); } } } public ForkJoinCalculator() { // 也可以使用公用的线程池 ForkJoinPool.commonPool(): // pool = ForkJoinPool.commonPool() pool = new ForkJoinPool(); } @Override public long sumUp(long[] numbers) { Long result = pool.invoke(new SumTask(numbers, 0, numbers.length - 1)); pool.shutdown(); return result; } } 输出: 耗时:390ms 结果为:50***00
26.3 函数式接口的用法?
-
Supplier接口 生成接口
- java.util.function.Supplier接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
import java.util.function.Supplier; public class Demo08Supplier { private static String getString(Supplier<String> function) { return function.get(); } public static void main(String[] args) { String msgA = "Hello"; String msgB = "World"; System.out.println(getString(() -> msgA + msgB)); } }
- Consumer接口 消费接口
- java.util.function.Consumer接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
- 抽象方法:accept
- 内部还有默认方法:andThen,可以连接多个Consumer接口,连续调用
import java.util.function.Consumer; public class Demo09Consumer { private static void consumeString(Consumer<String> function) { function.accept("Hello"); } public static void main(String[] args) { consumeString(s -> System.out.println(s)); consumeString(System.out::println); } } import java.util.function.Consumer; public class Demo10ConsumerAndThen { private static void consumeString(Consumer<String> one, Consumer<String> two) { one.andThen(two).accept("Hello"); } public static void main(String[] args) { consumeString( s -> System.out.println(s.toUpperCase()), s -> System.out.println(s.toLowerCase())); } }
- Predicate接口 判断接口
- 有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate接口。
- 抽象方法:test
- Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:
- 默认方法:and。用来表示与或非等关系,因此还有默认方法:or,negate
import java.util.function.Predicate; public class Demo15PredicateTest { private static void method(Predicate<String> predicate) { boolean veryLong = predicate.test("HelloWorld"); System.out.println("字符串很长吗:" + veryLong); } public static void main(String[] args) { // 条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。 method(s -> s.length() > 5); } } import java.util.function.Predicate; public class Demo16PredicateAnd { private static void method(Predicate<String> one, Predicate<String> two) { boolean isValid = one.and(two).test("Helloworld"); System.out.println("字符串符合要求吗:" + isValid); } public static void main(String[] args) { method(s -> s.contains("H"), s -> s.contains("W")); } }
- Function接口 转换接口
- java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。
- 抽象方法:apply
- Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。使用的场景例如:将String类型转换为Integer类型。
- 默认方法:andThen。用来完成组合操作
import java.util.function.Function; public class Demo11FunctionApply { private static void method(Function<String, Integer> function) { int num = function.apply("10"); System.out.println(num + 20); } public static void main(String[] args) { method(s -> Integer.parseInt(s));//Lambda表达式 method(Integer::parseInt);//方法引用 } } import java.util.function.Function; public class Demo12FunctionAndThen { private static void method(Function<String, Integer> one, Function<Integer, Integer> two) { int num = one.andThen(two).apply("10"); System.out.println(num + 20); } public static void main(String[] args) { method(Integer::parseInt, i -> i *= 10); } }
27. 访问控制关键字有哪些?
java类访问控制关键字 | ||||
---|---|---|---|---|
作用域 | 当前类 | 同包 | 子类 | 其他 |
Public | √ | √ | √ | √ |
Protected | √ | √ | √ | × |
Default(无) | √ | √ | × | × |
Private | √ | × | × | × |
- 使用场景
28. short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1; 有错吗?
29. Throw与thorws区别?
30. 什么时候使用 Integer,什么时候使用 int?
- Integer是int的包装类
- Integer不赋值的情况下式null,而int默认是0
- 因此,当需要区分出是否已经赋值和0的情况的时候,应该使用Integer
31. Integer的缓存机制?
public class CompareExample { public static void main(String[] args) { Integer num1 = new Integer(100); Integer num2 = new Integer(100); System.out.println(num1 == num2);// false System.out.println(num1 > num2);// 使用< > 号的时候会将包装类型转为基本类型 System.out.println(num1 < num2); } }
- 虽然对于-128~127的数有缓存,但是这里是直接new Interger对象,而==直接比较两个对象的内存地址,所以为false
public class CompareExample { public static void main(String[] args) { Integer num1 = new Integer(100); Integer num2 = new Integer(100); System.out.println("num1==num2 " + (num1 == num2));//false Integer num3 = 100; Integer num4 = 100; System.out.println("num3==num4 " + (num3 == num4));//true Integer num5 = 128; Integer num6 = 128; System.out.println("num5==num6 " + (num5 == num6));//false Integer num7 = 100; Integer num8 = new Integer(100); System.out.println("num7==num8 " + (num7 == num8));//false int num9 = 100; Integer num10 = new Integer(100); Integer num11 = 100; System.out.println("num9==num10 " + (num9 == num10));//true System.out.println("num9==num11 " + (num9 == num11));//true } }
-
第二处使用的是将数字直接赋值的方式来创建Integer,这种方式,在初始化的时候,等价与下面的代码:Integer num3 = Integer.valueOf(100)
-
此时会涉及到Integer的缓存机制
-
Integer是对小数据-128——127是有缓存的,在jvm初始化的时候,数据-128——127之间的数字便被缓存到了本地内存中,这样,如果初始化-128~127之间的数字,便会直接从内存中取出,而不需要再新建一个对象
-
对于num9==num10,当Integer与int进行比较的时候,会自动拆箱成为int进行比较
32. String str = new String("abc")究竟创建了几个对象?字符串新建问题
32.1 String str = "abc"创建对象的过程
-
首先在常量池中查找是否存在内容为"abc"字符串对象
-
如果不存在则在常量池中创建"abc",并让str引用该对象
-
如果存在则直接让str引用该对象
-
因此,只创建了一个对象,如果常量池一开始没有的话
32.2 String str = new String("abc")创建实例的过程
- 首先在堆中(不是常量池)创建一个指定的对象"abc",并让str引用指向该对象
- 在字符串常量池中查看,是否存在内容为"abc"字符串对象
- 若存在,则将new出来的字符串对象与字符串常量池中的对象联系起来
- 若不存在,则在字符串常量池中创建一个内容为"abc"的字符串对象,并将堆中的对象与之联系起来
32.3 String str1 = "abc"; String str2 = "ab" + "c"; str1==str2是true吗?
- **是。**因为String str2 = "ab" + "c"会查找常量池中是否存在内容为"abc"字符串对象,如存在则直接让str2引用该对象,显然String str1 = "abc"的时候,上面说了,会在常量池中创建"abc"对象,所以str1引用该对象,str2也引用该对象,所以str1==str2
- 字符串常量运算,最终结果也是字符串常量,存储在常量池中
32.4 String str1 = "abc"; String str2 = "ab"; String str3 = str2 + "c"; str1==str3是false吗?
- 是。因为String str3 = str2 + "c"涉及到变量(不全是常量)的相加,所以会生成新的对象,其内部实现是先new一个StringBuilder,然后 append(str2),append("c");然后让str3引用toString()返回的对象
- 涉及到了变量运算,最终的结果是一个新的对象
33. 枚举与普通类的区别?
- 相同点
- 都可以实现接口
- 不同点
- 枚举使用的是enum关键字,而不是class关键字
- 枚举类继承了Enum类,普通类继承了Object类
- 枚举的构造器只能使用private访问控制符
- 枚举的所有实例必须在枚举的第一行显示列出,否则这个枚举永远都不能生产实例,列出这些实例时系统会自动添加public static final修饰,无需程序员显式添加
- 所有的枚举类都提供了一个values方法,该方法可以很方便的遍历所有的枚举值。
- 常量值地址唯一,可以用==直接对比,性能会有提高。
34. 一个空对象占多少个字节?
-
一个对象在内存中的布局可以分为三个部分:
- 对象头
- 实例数据
- 对齐填充
-
对象头:
- 32位机器上为8字节,64位上为16字节(开启指针压缩为12字节)
- 数组对象在64位上为24字节,压缩后为16字节,因为它还要额外存储数组的长度
-
HotSpot虚拟机的对齐方式为8字节对齐
- 因此一个空对象最少占8字节(32位)
-
引用类型:
- 32位为4字节
- 64位为8字节(开启压缩为 4 bytes)
35. 对象头介绍一下?
- 对象头被精心设计为正好是8字节的倍数(1倍或者两倍,取决于虚拟机的位数)
36. 内存对齐的目的?
- 让字段只出现在同一个 CPU 的缓存行中,如果字段不对齐,就有可能出现一个字段的一部分在缓存行 1 中,而剩下的一半在 缓存行 2 中,这样该字段的读取需要替换两个缓存行,而字段的写入会导致两个缓存行上缓存的其他数据都无效,这样会影响程序性能.
- 通过内存对齐可以避免一个字段同时存在两个缓存行里的情况,但还是无法完全规避缓存伪共享的问题,也就是一个缓存行中存了多个变量,而这几个变量在多核 CPU 并行的时候,会导致竞争缓存行的写权限,当其中一个 CPU 写入数据后,这个字段对应的缓存行将失效,导致这个缓存行的其他字段也失效.
- 在 Disruptor 中,通过填充几个无意义的字段,让对象的大小刚好在 64 字节,一个缓存行的大小为64字节,这样这个缓存行就只会给这一个变量使用,从而避免缓存行伪共享,但是在 jdk7 中,由于无效字段被清除导致该方法失效,只能通过继承父类字段来避免填充字段被优化,而 jdk8 提供了注解@Contended 来标示这个变量或对象将独享一个缓存行,使用这个注解必须在 JVM 启动的时候加上 -XX:-RestrictContended 参数,其实也是用空间换取时间.
37. 浮点数的等值如何判断?BigDecimal
- 基本数据类型不能使用==来判断
- 包装类型也不能使用equals来判断
- 因为二进制无法精确表示大部分的十进制小数
- 因此浮点数进行比较时,一般指定一个误差范围,如果两个浮点数的差值在此范围之内,则认为两者是相等的
float a = 1.0F - 0.9F; float b = 0.9F - 0.8F; float diff = 1e-6F; if(Math.abs(a-b)<diff){ System.out.println("true"); }
- 也可以使用BigDecimal来定义值,再进行浮点数的运算操作
BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); BigDecimal x = a.subtract(b); BigDecimal y = b.subtract(c); if (x.compareTo(y) == 0) { System.out.println("true"); }
- 注意:
- BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法
- 因为equals方***比较值和精度(1.0 与 1.00 返回结果为 false) ,但是compareTo会忽略精度
38. BigDecimal特点介绍一下?
-
BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法
-
因为equals方***比较值和精度(1.0 与 1.00 返回结果为 false) ,但是compareTo会忽略精度
-
禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。
-
因为BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常
-
优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断
BigDecimal recommend1 = new BigDecimal("0.1"); BigDecimal recommend2 = BigDecimal.valueOf(0.1);
39. 为什么所有POJO类属性都应该使用包装数据类型?
- 数据库的查询结果可能为null值,如果使用基本数据类型,那么在进行自动拆箱的过程可能会报空指针异常
- 而且包装数据类型能够使用null值来表示额外信息
40. 泛型通配符的含义?
- 频繁往外读取内容的,适合用<? extends T>
- 该符号说明数据的类型继承自T,因此可以用T类型来接收从集合得到的数据
- 但是不能调用add方法,因为?号类继承自T,因此集合内部不知道使用什么类型来接收新进来的数据
- 上界
- 经常往里插入的,适合用<? super T>
- 该符号说明数据的类型均为T的父类,因此可以往集合里添加数据
- 但是不能调用get方法,因为?号类是T的父类,get出来的结果不知道用什么类型接收
- 下界
41. 内部类
41.1 内部类有哪几种分类?
41.2 静态内部类使用场景有哪些?
- 好处
- 一般定义包内可见(default修饰符)静态内部类
- 举例:
- ConcurrentHashMap中的Node静态内部类,继承了Map的Entry静态内部类,两个类属于同一个包,因此可以成功继承
- RenntrantLock中继承自AbstarctQueuedSynchronized的内部类Sync
- ThreadLocal静态内部类ThreadLocalMap中定义的内部类Entry
- 同一个包下的Thread内可以直接使用上述类
41.3 内部类创建方法?为什么局部内部类和匿名内部类访问局部变量时,变量需要加上final?
-
jdk7之前,匿名内部类访问局部变量加final修饰的问题(综合两种说法)_极客栈-CSDN博客_匿名内部类不能访问外部类未加final修饰的变量
-
静态内部类:
- new 外部类.静态内部类()
-
成员内部类:
- 外部类实例.new 内部类()
-
匿名内部类:
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- 匿名内部类不能定义任何静态成员和静态方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
-
局部内部类和匿名内部类访问局部变量时,变量需要加上final
- 避免生命周期不一
- 使用final能避免局部变量被回收,导致内部类调用的时候报错
-
原因:
- 一种说法:
- 内部类对象的生命周期可能超过局部变量的生命周期
- 当内部类对象没有引用再指向它的时候,内部类对象才会被回收。局部变量在方法调用结束,退栈的时候就会被回收
- 对于final修饰的局部变量,编译器会把匿名内部类对象要访问的所有final类型局部变量,都拷贝一份作为该对象的成员变量
- 这样,即使栈中局部变量已经死亡,匿名内部类对象照样可以拿到该局部变量的值,因为它自己拷贝了一份,且与原局部变量的值始终保持一致(final类型不可变)
- 在java8中,如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰
- 另外一种:
- 对于外部的引用对象,使用final进行修饰,是为了防止出现内部类对象中的引用于外部变量的引用指向不同对象的情况出现(防止内部类和外部环境不同步)
-
总结:
- 使用final修饰一方面是为了保证当局部内部类的生命周期大于局部变量的生命周期时,内部类仍然能访问局部变量
- 另外一方面是为了确保内部类引用的变量与外部的局部变量保持同步状态
- 举例:外部局部变量是一个引用,内部类使用该引用变量时,会将引用变量拷贝一份,如果外部局部变量没有使用final进行修饰,那么一旦外部变量修改了引用的话,那么外部变量引用的对象就跟内部类引用的对象不一致了
42. this与super的异同?
-
当this和super指代构造方法时,必须位于方法体的第一行.即,在一个构造方法中,this和super只能出现一个,且只能出现一次。否则会因子类调用多个父类构造方法而造成混乱
-
由于this和super都是在实例化阶段调用,因此不能出现在静态方法和静态代码块中
-
异同点如下图:
43. 类关系有哪几种?
-
继承 extends(is-a)
-
实现 implements(can-do)
-
组合 类是成员变量(contains-a)
- 强的整体与部分的关系。整体与部分同生共死,属于完全绑定关系
- 强于聚合
-
聚合 类是成员变量(has-a)
- 松散的聚合关系,可以拆分的整体与部分的关系
-
依赖 import 类(use-a)
- 除组合和聚合以外类与类之间的关系,这个类只要import,就是依赖关系
-
举例:
44. 构造方法的特点是什么?
45. 什么是重写?重写的特点?
46. 重载的顺序?
- 调用顺序
47. 什么是方法签名?如何决定重载哪个方法?
- 编译器将方法名称+参数类型+参数个数组成一个唯一键
- JVM通过这个唯一键决定调用哪个重载方法
48. 泛型的含义和目的?底层实现原理?
- 参数化类型
- 提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型
-
类型擦除
- 使用泛型的代码,在经过编译器编译时,编译器会将参数的类型擦除(参数、返回值类型),改为Object,尖括号的内容也没有了
- 但是在运行的时候会检查传进来的对象实例的类型是否匹配,不匹配就会报错
- 然后在结果返回的时候,会进行类型强制转换
-
使用好处见上
-
代码题
public static void main(String[] args) { List<String> l1 = new ArrayList<>(); List<Integer> l2 = new ArrayList<>(); Class<? extends List> c1 = l1.getClass(); Class<? extends List> c2 = l2.getClass(); System.out.println(c1 == c2); }
-
上述运行结果为true,因为编译器编译后,泛型会被擦除掉,替换为Object,因此最终得到的两个集合的类是同一个对象
-
使用案例
public class RpcResponse<T> implements Serializable { /** * 响应状态码 */ private Integer statusCode; /** * 响应状态补充信息 */ private String message; /** * 响应数据 */ private T data; public static <T> RpcResponse<T> success(T data) { RpcResponse<T> response = new RpcResponse<>(); response.setStatusCode(ResponseCode.SUCCESS.getCode()); response.setData(data); return response; } public static <T> RpcResponse<T> fail(ResponseCode code) { RpcResponse<T> response = new RpcResponse<>(); response.setStatusCode(code.getCode()); response.setMessage(code.getMessage()); return response; } }
48.1 List<A>类型的list,如何加入无继承关系的B类型对象?
- 通过反射方式调用相应的方法来直接添加
public class Test { public static void main(String[] args) throws Exception { ArrayList<A> list = new ArrayList<A>(); list.add(new A()); //这样调用 add 方法只能存储A,因为泛型类型的实例为 A // 反射添加 list.getClass().getMethod("add", Object.class).invoke(list, new B()); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } }
49. NIO与IO的区别,为什么要使用NIO?
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
无 | 选择器(Selectors) |
-
传统的IO是面向流的,是单向的,需要建立输出流和输入流
-
传统IO都是阻塞的
-
NIO是基于通道,面向缓冲区的
-
通道是指IO源与目标之间的连接,缓冲区用于存取数据
-
通道负责传输,缓冲区负责存储
-
NIO可以将通道配置为非阻塞,并且通过将通道注册到选择器上,能够实现对多路通道的监听
-
好处:
-
对于服务器而言,如果采用BIO的方式,大部分的线程都在第一阶段阻塞,等待数据准备
-
如果采用NIO的方式,就可以使用选择器来监听多路连接,当连接有数据到达时,才创建其它线程进行处理。可以更好地利用线程资源
50. JavaWeb是什么?
-
定义:
- 使用Java语言开发基于互联网的项目
-
软件架构
- C/S: Client/Server 客户端/服务器端
- B/S: Browser/Server 浏览器/服务器端
-
资源分类
- 静态资源
- 使用静态网页开发技术发布的资源
- 所有用户访问,得到的结果是一样的
- 如:文本,图片,音频、视频, HTML,CSS,JavaScript
- 动态资源
- 使用***页及时发布的资源
- 不同用户访问的结果可能不一样
- 如:jsp/servlet,php,asp...
- 如果用户请求的是动态资源,那么服务器会执行动态资源,转换为静态资源,再发送给浏览器
- 静态资源
-
静态资源
- HTML:用于搭建基础网页,展示页面的内容
- CSS:用于美化页面,布局页面
- JavaScript:控制页面的元素,让页面有一些动态的效果
51. JavaScript是什么?
-
客户端脚本语言
- 运行在客户端浏览器中的。每一个浏览器都有JavaScript的解析引擎
- 脚本语言:不需要编译,直接就可以被浏览器解析执行了
-
功能:
- 增强用户和html页面的交互过程,可以来控制html元素,让页面有一些动态的效果,增强用户的体验
-
JavaScript = ECMAScript(客户端脚本语言的标准) + JavaScript自己特有的东西(BOM+DOM)
- 概念:Browser Object Model 浏览器对象模型
- 概念: Document Object Model 文档对象模型
-
弱类型语言
- 强类型:在开辟变量存储空间时,定义了空间将来存储的数据的数据类型。只能存储固定类型的数据
- 弱类型:在开辟变量存储空间时,不定义空间将来的存储数据类型,可以存放任意类型的数据。
52. 框架概念?
- 一个半成品软件,开发人员可以在框架基础上,在进行开发,简化编码
53. xml与html的区别?
- xml标签都是自定义的,html标签是预定义
- xml的语法严格,html语法松散
- xml是存储数据的,html是展示数据
54. web服务器是什么?
- 服务器:安装了服务器软件的计算机
- 服务器软件:接收用户的请求,处理请求,做出响应
- web服务器软件:接收用户的请求,处理请求,做出响应。
- 在web服务器软件中,可以部署web项目,让用户通过浏览器来访问这些项目
- web容器
55. JavaEE是什么?
- Java语言在企业级开发中使用的技术规范的总和,一共规定了13项大的规范
56. Servlet是什么?特点是什么?执行原理?生命周期?
-
概念:运行在服务器端的小程序
- Servlet就是一个接口,定义了Java类被浏览器访问到(tomcat识别)的规则
- 将来我们自定义一个类,实现Servlet接口,复写方法
-
执行原理
- 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
- 查找web.xml文件,是否有对应的<url-pattern>标签体内容
- 如果有,则在找到对应的<servlet-class>全类名
- tomcat会将字节码文件加载进内存,并且创建其对象
- 调用其方法
-
Servlet中的生命周期
- 被创建:执行init方法,只执行一次
- Servlet什么时候被创建?
- 默认情况下,第一次被访问时,Servlet被创建
- 可以配置执行Servlet的创建时机
- 在<servlet>标签下配置
- 第一次被访问时,创建
- <load-on-startup>的值为负数
- 在服务器启动时,创建
- <load-on-startup>的值为0或正整数
- Servlet什么时候被创建?
- Servlet的init方法,只执行一次,说明一个Servlet在内存中只存在一个对象,Servlet是单例的
- 多个用户同时访问时,可能存在线程安全问题。
- 解决:尽量不要在Servlet中定义成员变量。即使定义了成员变量,也不要修改值
- 提供服务:执行service方法,执行多次
- 被销毁:执行destroy方法,只执行一次
- Servlet被销毁时执行。服务器关闭时,Servlet被销毁
- 只有服务器正常关闭时,才会执行destroy方法
- 被创建:执行init方法,只执行一次
-
Servlet3.0
- 支持注解配置。可以不需要web.xml了
-
使用方法:
- 定义一个类,实现Servlet接口
- 复写方法
- 在类上使用@WebServlet注解,进行配置
- @WebServlet("资源路径")
57. HttpServlet是什么?
- 抽象类
- 继承于GenericServlet继承于Servlet
- 对http协议的一种封装,简化操作
- 定义类继承HttpServlet
- 复写doGet/doPost方法
- 当然也有其他与http请求方式对应的方法,不过上面两种使用较多
58. Request和Response是什么?
- 对象原理
- request和response对象是由服务器(tomcat)创建的。我们来使用它们
- request对象是来获取请求消息,response对象是来设置响应消息
- 继承体系
58.1 Request特点介绍一下?
-
解决中文乱码
- get方式:tomcat 8 已经将get方式乱码问题解决了
- post方式:在获取参数前,设置request的编码request.setCharacterEncoding("utf-8");
-
请求转发:一种在服务器内部的资源跳转方式
- 通过request对象获取请求转发器对象:RequestDispatcher getRequestDispatcher(String path)
- 使用RequestDispatcher对象来进行转发:forward(ServletRequest request, ServletResponse response)
-
特点
- 浏览器地址栏路径不发生变化
- 只能转发到当前服务器内部资源中。
- 转发是一次请求
-
共享数据
- request域:代表一次请求的范围,一般用于请求转发的多个资源***享数据
- 方法:
- void setAttribute(String name,Object obj):存储数据
-
获取ServletContext:
- ServletContext getServletContext()
58.2 Response特点介绍一下?
-
设置响应消息
-
重定向
- 乱码问题
- response.setContentType("text/html;charset=utf-8");
59. ServletContext介绍一下?
- 代表整个web应用,可以和程序的容器(服务器)来通信
- 个人理解可以代表是tomcat服务器
- 获取方式:
- 通过request对象获取
- request.getServletContext();
- 通过HttpServlet获取
- this.getServletContext();
- 通过request对象获取
- 作用:
- 获取域对象的共享数据
- 获取文件的真实(服务器)路径
60. 什么是会话?
- 一次会话中包含多次请求和响应
- 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止
- 在一次会话的范围内的多次请求间,共享数据
- cookie和session
61. 什么是cookie?
- 默认浏览器关闭的时候被销毁,默认值为负值,如果为正值,那么时间到了就自动失效,0则直接删除
- 通过setMaxAge来设置
62. 什么是Session?
- 服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession
-
Session的实现依赖于cookie
-
session.invalidate(),强制清除session
-
否则,从最后一次访问起,过了30分钟会自动清除,如果中途有访问,则刷新时间
63. 什么是JSP?
- 概念:
- Java Server Pages: java服务器端页面
- 一个特殊的页面,其中既可以指定定义html标签,又可以定义java代码,用于简化书写
- 原理:
- 本质上就是一个Servlet
- JSP内置对象:
- 一共有9个
64. 什么是动态代理?底层原理是什么?
- JDK动态代理
- 被代理对象需要实现接口
- 通常实现接口的方式来创建代理对象
- 底层原理
- 利用反射机制生成一个实现代理接口的匿名类
- 在调用具体的方法之前调用InvocationHandler
public static void main(String[] args) { //1.创建真实对象 Lenovo lenovo = new Lenovo(); //2.动态代理增强lenovo对象 /* 三个参数: 1. 类加载器:真实对象.getClass().getClassLoader() 2. 接口数组:真实对象.getClass().getInterfaces() 3. 处理器:new InvocationHandler() */ SaleComputer proxy_lenovo = (SaleComputer) Proxy.newProxyInstance(lenovo.getClass().getClassLoader(), lenovo.getClass().getInterfaces(), new InvocationHandler() { /* 代理逻辑编写的方法:代理对象调用的所有方法都会触发该方法执行 参数: 1. proxy:代理对象 2. method:代理对象调用的方法,被封装为的对象 3. args:代理对象调用的方法时,传递的实际参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /*System.out.println("该方法执行了...."); System.out.println(method.getName()); System.out.println(args[0]); */ //判断是否是sale方法 if(method.getName().equals("sale")){ //1.增强参数 double money = (double) args[0]; money = money * 0.85; System.out.println("专车接你...."); //使用真实对象调用该方法 String obj = (String) method.invoke(lenovo, money); System.out.println("免费送货..."); //2.增强返回值 return obj+"_鼠标垫"; }else{ Object obj = method.invoke(lenovo, args); return obj; } } }); //3.调用方法 /* String computer = proxy_lenovo.sale(8000); System.out.println(computer);*/ proxy_lenovo.show(); }
-
cglib动态代理
- 通过继承的方式来生成代理类
-
底层原理
- 利用asm开源包
- 将被代理类的class文件加载进来
- 通过修改字节码生成子类来处理
-
依赖
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.9.RELEASE</version> </dependency>
- 实例
public class MetaspaceOutOfMemoryDemo { // 静态类 static class OOMTest { } public static void main(final String[] args) { // 模拟计数多少次以后发生异常 int i =0; try { while (true) { i++; // 使用Spring的动态字节码技术 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMTest.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, args); } }); } } catch (Exception e) { System.out.println("发生异常的次数:" + i); e.printStackTrace(); } finally { } } }
65. 什么是Filter(过滤器)?
-
当访问服务器的资源时,过滤器可以将请求拦截下来,完成一些特殊的功能
-
作用:
- 一般用于完成通用的操作。如:登录验证、统一编码处理、敏感字符过滤...
-
使用方法:
- 定义一个类,实现接口Filter
- 复写方法
- 注解配置拦截路径
- @WebFilter("/*")//访问所有资源之前,都会执行该过滤器
-
执行流程:
- 执行过滤器
- 执行放行后的资源(放行后的代码)
- 回来执行过滤器放行代码下边的代码
-
过滤器链
66. 什么是Listener(***)?
- 监听机制:
- 将事件、事件源、***绑定在一起。 当事件源上发生某个事件后,执行***代码
- 举例:
- 监听ServletContext对象的创建和销毁
- 步骤:
- 定义一个类,实现ServletContextListener接口
- 复写方法
- 注解:@WebListener(或者使用配置文件)
67. 什么是JQuery?
- JavaScript框架,用来简化JS开发
- 本质上就是一些js文件,封装了js的原生代码而已
68. 什么是AJAX?
- ASynchronous JavaScript And XML 异步的JavaScript 和 XML
- Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术
69. 什么是JSON?
- 概念: JavaScript Object Notation JavaScript对象表示法
- 作用:
- json现在多用于存储和交换文本信息的语法
- 进行数据的传输
- JSON 比 XML 更小、更快,更易解析。
70. java程序执行流程?
- Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
71. switch支持哪些类型?
-
Java5以前:byte,short,char,int
-
Java5:枚举类型加入
-
Java7:String类型加入
-
byte,short,char,int
-
上面的包装类型
- 底层靠的是编译器的自动拆箱
-
枚举类
- ordinal方法返回一个int类型的值
-
String
- hashcode方法返回一个int型
72. static的存在意义是什么?
- 创建独立于具体对象的域变量和方法。
- 使得在没有创建对象的情况下,也能使用属性和调用方法
- 创建静态代码块来优化代码性能
73. 类的空参构造器作用是什么?
- 子类继承父类后,当子类创建对象实例时,会调用父类的空参构造方法(没有指定的情况下)
- 目的是帮助子类做初始化工作
74. IO流有几类?
-
输出和输入
-
字节和字符
-
节点流和处理流
-
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
-
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
75. instanceof关键字的作用是什么?
-
用来测试一个对象是否为一个类的实例
-
boolean result = obj instanceof Class
-
编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定
-
obj必须为引用类型,否则编译不通过
-
编译的判断是根据obj能否强制类型转换为右边的类,如果可以,编译器不会报错
76. 自动装箱拆箱是什么?原理?
- 从1.5开始支持
- 底层是依靠编译器来完成功能的
- 编译器会将装箱阶段自动替换为了 valueOf 方法,拆箱阶段自动替换为了 xxxValue 方法
77. i = i++ 问题
- 对于i=i++:
- 由于是i++,因此先将i的值加载到操作数栈中
- 然后调用incc自增变量i
- 最后再将操作数栈的栈顶赋值给变量i
- 因此最后变量i为1
- 注意:自增和自减都是直接修改变量的值,无须经过操作数栈
78. JUC你用过哪些包?
- ConcurrentHashMap
- CopyOnWriteArrayList
- Executors
- 创建线程池
- 并发工具
- countdownlaunch
- cyclicbarrier
- semaphore
- ThreadPoolExecutor
- 使用最多的线程池创建工具
- 各种阻塞队列
79. 枚举类如何定义实例对象?
- 在定义枚举类的同时,在首行定义
- valueOf(String str)将字符串转为对应的枚举类对象
80. 注解原理是什么?
- 注解本质是一个继承了Annotation的特殊接口
- 运行的时候会在内存中生成注解的动态代理类
- 通过反射获取的就是生成的动态代理对象
81. Java存在内存泄漏吗?
-
长生命周期的容器,存放了短生命周期的对象。这些对象后面可能不再用到,但是由于容器一直持有对象的引用,因此对象不能被回收
-
hashset中的元素的字段进行了修改,导致元素的hash值发生了变化,集合无法单独删除该元素
-
静态集合类
- 静态容器中,保存了一些短生命周期的对象,这些对象在以后可能不再需要使用了,但是由于容器仍然持有这些对象的引用,因此对象不能被垃圾收集器回收
-
各种的连接。如数据库连接、IO连接等
- 以数据库为例,当对数据库进行操作时,首先需要与数据库建立连接,当不再使用连接的时候,需要关闭连接,只有这样才能回收相应的对象,否则会造成大量的对象无法回收,引起内存泄漏
-
变量的作用域不合理
- 如果一个变量的作用域大于它实际需要的范围,而且变量使用完没有置为空的话,就会出现内存泄漏的情况
-
内部类持有外部类
- 成员内部类需要通过外部类的实例来进行对象创建,如果内部类的对象长期引用,那么就算外部类的对象不再使用,也不能被回收
-
改变哈希值
- 一个对象被存进hashset后,不能再修改参与计算哈希值的字段
- 否则一旦字段修改后,对象的哈希值发生变化,此时无法再根据当前对象的引用来检索集合中的目标对象,导致无法从集合中单独删除该对象,造成内存泄漏
-
数组中不用的对象没有及时置空
- 对象数组中一直保存着不用的对象的引用导致对象无法被回收
-
缓存泄漏
- 将数据存放到本地缓存中,但是后面遗忘了
- 可以通过WeakHashMap解决,该map存储的对象在垃圾回收的时候会被回收掉
82. 如何定位内存泄漏以及分析问题原因?
- 可以通过jstat -gcutil pid查看GC信息统计
-
当存在大量的FGC的时候,就有可能是内存泄漏的问题
-
也可以通过可视化工具
- visualVM来查看垃圾回收以及堆内存的情况
83. equals和hashcode方法重写
- equals
@Override public boolean equals(Object o) { // 如果是相同对象 if (this == o) return true; // 如果类不同 if (o == null || getClass() != o.getClass()) return false; // 先进行类型强制转换 EqualAndHashCode that = (EqualAndHashCode) o; // 再对属性值进行比较 return a == that.a && b == that.b; }
- hashcode
@Override public int hashCode() { return Objects.hash(a, b); } public static int hash(Object... values) { return Arrays.hashCode(values); } // 对于多个属性值,会计算每个属性值的hashcode值,然后通过数***算得出最终对象的hashcode值 public static int hashCode(Object a[]) { if (a == null) return 0; int result = 1; for (Object element : a) result = 31 * result + (element == null ? 0 : element.hashCode()); return result; }
- 这里选择了31作为乘子,原因如下:
- 31是一个不大不小的质数
- 使用质数能够降低哈希算法的冲突率
- 数值太大容易导致hash值溢出,太小容易产生hash冲突(分布性不好,比较集中于小数值范围)
- 31可以被JVM优化,31 * i = (i << 5) - i
84. 浅拷贝和深拷贝的异同?
- 浅拷贝:只拷贝了源对象的地址,当源对象发生变化时,拷贝对象也会发生变化
- 深拷贝:拷贝了源对象的所有值,以及源对象的内部引用,即使源对象发生变化,拷贝对象也不会发生变换
85. 如何实现深拷贝?
- 定义一个用于深拷贝的构造函数
- 基本类型和字符串,直接赋值
- 对象则直接新建一个
- 重写clone方法
- 目标类需要实现Cloneable接口,表明该类支持深拷贝
- 然后重写object类的clone方法
- 对于只有基本类型和字符串类型的类:
- return (Address) super.clone();
- 调用父类的clone方法,并将结果强制转换为目标类
- 对于包含引用类型的类:
- user.setAddress(this.address.clone());
- 调用引用类型的clone方法来创建一个新对象
- 注意,super.clone()方法本身是浅拷贝,因此对于引用类型的变量,只会复制对象的地址,所以对于引用变量,还要额外调用该变量的clone方法来重新赋值
/** * 地址 */ public class Address implements Cloneable { private String city; private String country; // constructors, getters and setters @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } } /** * 用户 */ public class User implements Cloneable { private String name; private Address address; // constructors, getters and setters @Override public User clone() throws CloneNotSupportedException { User user = (User) super.clone(); user.setAddress(this.address.clone()); return user; } }
- 序列化机制
- 先将源对象序列化,然后再反序列化生成拷贝对象
- 需要源对象实现Serializable接口(如果用JDK自带的序列化机制)
- 也可以先序列化为json,然后再反序列化
86. 序列化底层原理是什么?
- 首先会判断要序列化的对象是否实现了Serializable接口(String、array、enum都可以直接序列化),否则抛异常
- 然后再递归获取类的基本类型数据和引用类型数据的信息,然后写入数据
87. 如何避免敏感字段被序列化?
- static和transient修饰的字段不会被序列化
88. serialVersionUID的作用是什么?
- 在反序列化的时候,会根据该UID判断要反序列化的目标类是否跟源类相同,如果相同,则反序列化成功,否则失败
- 如果没有设置的话,会根据当前类的属性生成
- 因此,如果一个类要想进行兼容升级的话,不要修改UID,否则,修改UID
89. String不可变的好处是什么?
- 可以用来实现字符串常量池
- 使得内容一样的不同的字符串变量都指向常量池中的同一个字符串
- 减少内存开销
- 字符串的hashcode固定,无需重新计算
- 适合作为hash表的key
- string是不可变的,是线程安全的
90. 反射效率低的原因是什么?
-
检索方法时,需要从不连续的堆中检索代码段,定位函数入口
-
执行invoke方法需要对参数进行封装和解封操作
- invoke方法的参数是Object[]类型,因此,需要先将函数的参数封装成该类型
- 但是在内部使用的时候,还需要将参数恢复成没有被Object[]包装前的样子,而且还需要对参数进行校验,这里就使用到了解封操作
-
需要检查方法的可见性
- 反射每次调用方法时需要检查方法的可见性
-
需要校验方法参数
- 需要检查实参与形参的类型匹配性
-
反射方法难以内联
-
JIT无法优化
91. 如何优化反射效率?
-
可以使用高性能的反射工具包
- 譬如说ReflectASM
-
一般反射的流程是先使用getMethod,然后使用invoke调用相应的方法
-
其中getMethod需要在不连续的堆中检索代码段、定位函数入口
-
由于代码段属于类空间,类被加载后,一般来讲,除非虚拟机关闭,不然函数入口不会发生变化。
-
因此对于频繁反射调用相同的函数的场景,可以将这些函数的入口缓存起来,减少定位造成的资源浪费
92. Java反射原理是什么?
-
本质:绕开编译器,在运行期直接从虚拟机获取对象实例/访问对象成员变量/调用对象的成员函数。
-
特点:
- 反射模式会更加抽象,而且更加繁琐
- 反射绕开了编译器的合法性检查
- 效率低于传统模式(不使用反射调用)
-
在linux中只要知道一个变量的起始地址就可以读出这个变量的值,因为从这个起始地址起前8位记录了变量的大小,也就是可以定位到结束地址,在 Java 中我们可以通过 Field.get(object) 的方式获取变量的值,也就是反射,最终是通过 UnSafe 类来实现的.我们可以分析下具体代码
Field 对象的 getInt方法 先安全检查 ,然后调用 FieldAccessor @CallerSensitive public int getInt(Object obj) throws IllegalArgumentException, IllegalAccessException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } return getFieldAccessor(obj).getInt(obj); } 获取field在所在对象中的地址的偏移量 fieldoffset UnsafeFieldAccessorImpl(Field var1) { this.field = var1; if(Modifier.isStatic(var1.getModifiers())) { this.fieldOffset = unsafe.staticFieldOffset(var1); } else { this.fieldOffset = unsafe.objectFieldOffset(var1); } this.isFinal = Modifier.isFinal(var1.getModifiers()); } UnsafeStaticIntegerFieldAccessorImpl 调用unsafe中的方法 public int getInt(Object var1) throws IllegalArgumentException { return unsafe.getInt(this.base, this.fieldOffset); }
- 通过上面的代码我们可以通过属性相对对象起始地址的偏移量,来读取和写入属性的值,这也是 Java 反射的原理,这种模式在jdk中很多场景都有用到,例如LockSupport.park中设置阻塞对象.
- 那么属性的偏移量具体根据什么规则来确定的呢? 下面我们借此机会分析下 Java 对象的内存布局
93. Java和C++的区别是什么?
- 相同点
- 两者都是面向对象的语言,支持封装、继承、多态
- 不同点
- C++支持多继承,并且可以使用指针直接访问内存,由程序员自己管理内存
- Java是单继承,不提供指针访问内存,接口可以实现多继承,通过JVM的内存管理机制来管理内存
94. 多态实现原理是什么?
-
底层使用动态绑定
-
动态绑定主要应用于虚方法和接口方法
-
虚方法的方法调用与方法实现的关联分为两种
- 一种是在编译期确定,被称为静态分派,比如方法的重载
- 一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)
- 对象方法基本上都是虚方法。
-
多态的实现
-
虚拟机栈中会保存当前方法调用的栈帧
-
程序运行中,会通过栈帧的信息找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用过程
-
具体步骤
- 从操作栈中找到对象的实际类型class
- 在class中找到与被调用方法签名相同的方法,有访问权限就直接返回,否则报错
- 如果没有找到相应方法,那么就去父类寻找
- 如果溯源都找不到,那么报错
-
从上述过程可以看出,动态分派会先找到子类方法
95. Java中如何阻止类的继承?
- 使用final关键字修饰类
- 将类的构造器私有化
- 类的构造器私有化后,子类无法调用父类的构造器方法,因此无法继承该类
96. Java流式API介绍一下?
-
使用流式处理能简化集合操作
-
stream()
- 将集合转换成一个流
-
filter()
- 执行自定义的筛选处理,可以使用lambda表达式
-
collect()
- 对结果进行封装处理,一般配合Collectors.toList()将结果封装为list集合返回
-
sorted()
- 对列表进行排序
-
limit()
- 返回前几个元素
-
distinct()
- 去重,基于equals
-
skip()
- 跳过指定个数的元素
-
map()
- 映射操作
-
sum()
- 求和
-
count()
- 计数
-
数组转流
- Arrays.stream(array)
- Stream.of(array)
-
字符串转流
-
IP.chars().filter(ch -> ch == '.').count()
-
97. 为什么Java语言的性能比C++差?
- 因为Java语言是半编译语言,编译的结果不是机器码,而是字节码,需要JVM解释执行
- JVM会将热点代码编译成机器码,提高执行效率
- C++编译结果是机械码,可以直接执行,效率更高
98. ClassNotFoundException出现在哪些地方?
- 调用Class的forName方法时,找不到指定的类
- ClassLoader 中的 findSystemClass() 方法时,找不到指定的类
- ClassLoader 中的 loadClass() 方法时,找不到指定的类
99. 为什么加载数据库驱动要用Class.forName()
-
一篇文章吃透:为什么加载数据库驱动要用Class.forName()_明明如月的技术博客-CSDN博客_class.forname
-
Class.forName():将类的.class文件加载到jvm中之外,还会对类进行初始化,执行类中的static代码块
-
ClassLoader.loadClass():只会将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块
-
new:【加载class文件到jvm + 初始化static代码块】(Class.forName) +构造实例(newInstance)
-
在数据库驱动的代码中,是通过静态代码块来对驱动进行注册的,因此需要使用前者
100. 常量池的特点介绍一下?
-
参考
-
1.全局常量池在每个JVM中只有一份,存放的是字符串常量的引用值。
-
2.class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
-
3.运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
101. JVM如何处理异常?
-
JVM如何捕获异常
- 编译而成的字节码中,每个方法都附带一个异常表。
- 异常表中每一个条目代表一个异常处理器
- 触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针的范围内。
- 范围匹配后,会去比较异常类型和异常处理器中的type是否相同。
- 类型匹配后,会跳转到target指针所指向的字节码(catch代码块的开始位置)
- 如果没有匹配到异常处理器,会弹出当前方法对应的Java栈帧,并对调用者重复上述操作。
-
如下图所示是异常表,每一项对应了一个异常处理器
- 指针的数值就是字节码的索引,可以用来直接定位字节码
-
如果在当前方法的异常表中没有匹配到异常处理器,那么会
- 会弹出当前方法对应的Java栈帧
- 在调用者上重复异常匹配的流程。
- 最坏情况下,JVM需要遍历当前线程Java栈上所有方法的异常表
-
finally代码块的编译
- 复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中
-
为什么捕获异常会较大的性能消耗?
- 因为构造异常的实例比较耗性能。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。虽然具体不清楚JVM的实现细节,但是看描述这件事情也是比较费时费力的。
- 构造异常实例需要记录下当前线程的栈帧,记录下各种调试信息,因此比
如果对你有帮助的话,还请大家评论、收藏、点赞来支持一下!