最近找工作,复习了下java相关的知识。发现已经对很多概念模糊了。记录一下。部分是往年面试题重新整理,部分是自己面试遇到的问题。持续更新中~
目录
- java相关
- 1. 面向对象设计原则
- 2. 面向对象的特征是什么
- 3. 重载和重写
- 4. 基本数据类型
- 5. 装箱和拆箱
- 6. final 有什么作用
- 7. String是基本类型吗,可以被继承吗
- 8. String、StringBuffer和StringBuilder的区别?
- 8. 抽象类和接口的区别
- 9. String 类的常用方法都有那些?
- 10. Java 中 IO 流分为几种?
- 11. Java容器有哪些
- 12. Collection 和 Collections 有什么区别?
- 13. HashMap 和 Hashtable 有什么区别?
- 14. 如何决定使用 HashMap 还是 TreeMap?
- 15. 说一下 HashMap 的实现原理?
- 16. equals和 == 的区别
- 17. ConcurrentHashMap如何实现线程安全的
- 18. 说一下 HashSet 的实现原理?
- 19. ArrayList 和 LinkedList 的区别是什么?
- 20. ArrayList 和 Vector 的区别是什么?
- 21. Array 和 ArrayList 有何区别?
- 22. 在 Queue 中 poll()和 remove()有什么区别?
- 多线程
- 23. 并行和并发有什么区别?
- 24. 线程和进程的区别?
- 25. 守护线程是什么?
- 26. 创建线程有哪几种方式?
- 27. 说一下 runnable 和 callable 有什么区别?
- 28. 线程有哪些状态?
- 29. sleep() 和 wait() 有什么区别?
- 30. notify()和 notifyAll()有什么区别?
- 31. 线程的 run() 和 start() 有什么区别?
- 32. 创建线程池有哪几种方式?
- kotlin
- android
- flutter
java相关
1. 面向对象设计原则
- 面向对象设计原则
- 单一职责原则——SRP
一个类的职责尽量单一,清晰。即一个类最好专注做一件事情,而不是分散的做好几件事。
每个类都只负责一项任务,可以降低类的复杂性;提高可读性;提高系统可维护性;避免类的臃肿和功能太多太复杂。 - 依赖倒置原则——DIP
实现时尽量依赖抽象,而不依赖于具体实现。
可以减少类间的耦合性,提高系统稳定性
提高代码的可读性,可维护性以及扩展性。 - 接口隔离原则——ISP
即应当为客户端提供尽可能小的单独的接口,而不是大而杂的接口。
也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。 - 里氏替换原则——LSP
即超类存在的地方,子类是可以替换的。
里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。 - 迪米特原则——LOD
即一个软件实体应当尽可能少的与其他实体发生相互作用。
一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用,在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略。 - 开闭原则——OCP
面向修改关闭,面向扩展开放。
即一个软件、一套系统在开发完成后,当有增加或修改需求时,应该对拓展代码打开,对修改原有代码关闭
2. 面向对象的特征是什么
- 封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 - 继承
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”。
被继承的类称为“基类”、“父类”或“超类”。
继承的过程,就是从一般到特殊的过程。
要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。 - 多态
就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
实现多态一般通过重写和重载
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。
3. 重载和重写
- 重写
是指子类重新定义父类的虚函数的做法。需要保持参数个数,类型,返回类型完全一致。
属于运行时多态的表现 - 重载
是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)
其实,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)
4. 基本数据类型
数据类型 | 位数 | 默认值 | 取值范围 | 示例 |
---|---|---|---|---|
byte | 1字节 | 0 | -2(128~127) | byte a = 10; |
short | 2字节 | 0 | -2(-32768~32767) | short a = 10; |
int | 4字节 | 0 | (-2147483648~2147483647) | int a = 10; |
float | 4字节 | 0.0 | -2(-2147483648~2147483647) | float a = 10; |
long | 8字节 | 0 | -2(-9223372036854774808~9223372036854774807) | long a = 10; |
double | 8字节 | 0.0 | -2(-9223372036854774808~9223372036854774807) | char a = 10; |
char | 2字节 | 空 | -2(128~127) | char a = ‘a’; |
boolean | 1字节 | false | true、false | booleana = true; |
特殊类型void |
对应的包装类
byte——Byte
short——Short
int ——Integer
float——Float
long——Long
double——Double
char——Character
boolean——Boolean
void——Void
包装类出现的原因是Java语言是面对对象的编程语言,而基本数据类型声明的变量并不是对象,为其提供包装类,增强了Java面向对象的性质。
void是一个特殊的类型,有人把它归到Java基本数据类型中,是因为可以通过Class.getPrimitiveClass(“void”)获取对应的原生类型。
void有个对应的类型Void,可以把它看做是void的包装类,Void的作用主要作用有以下2个:
- 泛型占位
当我们定义了泛型时,如果没有写泛型参数,idea等开发工具会给出提醒,建议写上泛型类型,但实际上却是不需要固定的泛型类型,这时候据可以写上Void来消除警告,例如ResponseData - 反射获取void方法
Method[] methods = String.class.getMethods();
for (Method method : methods) {
if(method.getGenericReturnType().equals(Void.TYPE)){
System.out.println(method.getName());
}
}
//输出:
//getBytes
//getChars
//wait
//wait
//wait
//notify
//notifyAll
5. 装箱和拆箱
将基本数据类型转化为包装类就叫做装箱;
调用 包装类.valueOf()方法
int a = 22;
//装箱 在实例化时候进行装箱
Integer inter1 = new Integer(a);
//装箱 调用valueOf方法进行装箱
Integer inter2 = Integer.valueOf(a);
valueOf 方法是一个静态方法,直接通过类进行调用
拆箱
将包装类转化为基本数据类型;
调用 包装类.parseXXX()方法
int a = Integer.parseInt("3");
6. final 有什么作用
- final 修饰的类叫最终类,该类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
7. String是基本类型吗,可以被继承吗
String不是基本类型,不可以被继承。因为String被final关键字修饰,不可以被继承
public final class String implements Serializable, Comparable<String>, CharSequence {
8. String、StringBuffer和StringBuilder的区别?
String 大小固定数不可变的。
因为String是字符串常量,是不可变的。实际的拼接操作最后都是产生了一个新的对象并存储这个结果
String str1 = "123";
String str2 = "123"//实际上 "123"这个字符串在常量池中只有一份,str1,str2 两个对象都是指向"123"
str2 = str2 + "45";
实际上是产生了个新的String对象存放"12345"这个结果
查看字节码实际代码是
0 ldc #2 <123>
2 astore_1
3 new #3 <java/lang/StringBuilder>
6 dup
7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
10 aload_1
11 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
14 ldc #6 <45>
16 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
22 astore_1
23 return
//实际也是通过 StringBuilder.append方法实现拼接,然后toString的性能比较低
//所以字符串拼接直接使用StringBuilder实现效率更高
StringBuffer 大小可变,线程安全(有锁),同步,效率低,适用于多线程,低并发。
StringBuilder 大小可变,线程不安全(无锁),不同步,效率高,适用于单线程,高并发。
8. 抽象类和接口的区别
- 抽象类是对事物属性的抽象,接口是对行为的抽象,是一种规范或者说行为约束。
- 抽象类关键词abstract 接口关键字interface
- 抽象类可以有成员变量,普通方法和抽象方法,接口只能有抽象方法(只有方法定义,无具体函数实现)
- 抽象类可以有构造方法,接口没有构造方法
- 继承了抽象类的子类,要么对父类的抽象方法进行重写,要么自己也是抽象类
- 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
-
9. String 类的常用方法都有那些?
- inexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
-
10. Java 中 IO 流分为几种?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。11. Java容器有哪些
Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示: - Collection
- List
- ArrayList
- LinkedList
- Vector
- Stack
- Set
- HashSet
- LinkedHashSet
- TreeSet
- List
- Map
- HashMap
- LinkedHashMap
- TreeMap
- ConcurrentHashMap
- Hashtable
- HashMap
12. Collection 和 Collections 有什么区别?
- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
- Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections. sort(list)。
13. HashMap 和 Hashtable 有什么区别?
- 存储:HashMap 允许 key 和 value 为 null,而 Hashtable 不允许。
- 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
- 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
14. 如何决定使用 HashMap 还是 TreeMap?
对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择
15. 说一下 HashMap 的实现原理?
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
jdk1.7以前使用数组+链表实现HashMap,jdk1.8以后用数组+红黑树实现。当链表长度较短时使用链表,长度达到阈值时自动转换为红黑树,提高查询效率。
- 其它问题
- 默认大小 16 ,负载因子0.75 大小到达12时 自动两倍扩容。
16. equals和 == 的区别
- 对于 == 来说:
如果比较的是基本数据类型变量,比较两个变量的值是否相等。(不一定数据类型相同)
如果比较的是引用数据类型变量,比较两个对象的地址值是否相同,即两个引用是否指向同一个地址值 - 对于 equals 来说:
如果类中重写了equals方法,比较内容是否相等。
String、Date、File、包装类都重写了Object类的equals方法。
如果类中没有重写equals方法,比较地址值是否相等(是否指向同一个地址值)。
Student stu1 = new Student(11, "张三");
Student stu2 = new Student(11,"张三");
System.out.println(stu1.equals(stu2));//false
既然equals比较的是内容是否相同,为什么结果还是false呢?
回顾知识:
在Java中我们知道任何类的超类都是Object类,Student类也继承Object类。
查看Object类中的equals方法也是 == 比较(也就是比较地址值),因此结果当然是false。
public boolean equals(Object obj) {
return (this == obj);
}
既然这样我们如何保证两个对象内容相同呢?
这里就需要我们去重写equals方法?
@Override
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj instanceof Student) {
Student stu = (Student)obj;
return this.age == stu.age && this.name.equals(stu.name);
}
return false;
}
17. ConcurrentHashMap如何实现线程安全的
-
jdk 1.7以前结构是segment数组 + HashEntry数组 + 链表,使用分段式锁,实现线程安全。容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁”思想,见下图:
get()操作:
HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。
1.将key传入get方法中,先根据key的hashcode的值找到对应的segment段。
2.再根据segment中的get方法再次hash,找到HashEntry数组中的位置。
3.最后在链表中根据hash值和equals方法进行查找。
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
put()操作:
1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段
2.再根据segment中的put方法,加锁lock()。
3.再次hash确定存放的hashEntry数组中的位置
4.在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。 -
jdk1.8以后结构是 数组+Node+红黑树实现,采用**Synchronized + CAS(自旋锁)**保证线程安全。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS
为什么在有Synchronized 的情况下还要使用CAS
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized
来保证线程安全,大大提高了低并发下的性能.
锁 :
锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容.
读操作无锁 :
-
Node的val和next使用volatile修饰,读写线程对该变量互相可见
-
数组用volatile修饰,保证扩容时被读线程感知
-
get()操作:
get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。
在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。
1.计算hash值,定位到Node数组中的位置
2.如果该位置为null,则直接返回null
3.如果该位置不为null,再判断该节点是红黑树节点还是链表节点
如果是红黑树节点,使用红黑树的查找方式来进行查找
如果是链表节点,遍历链表进行查找 -
put()操作:
1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();
2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
5.如果是红黑树,就按照红黑树的结构进行插入。
18. 说一下 HashSet 的实现原理?
- HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
19. ArrayList 和 LinkedList 的区别是什么?
- 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
- 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数
- 存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
20. ArrayList 和 Vector 的区别是什么?
- 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
- 性能:ArrayList 在性能方面要优于 Vector。
- 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
21. Array 和 ArrayList 有何区别?
- Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
- Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
- Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
22. 在 Queue 中 poll()和 remove()有什么区别?
- 相同点:都是返回第一个元素,并在队列中删除返回的对象。
- 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
多线程
23. 并行和并发有什么区别?
- 并行:多个处理器或多核处理器同时处理多个任务。
- 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
24. 线程和进程的区别?
- 进程是cpu资源分配的基本单位
- 线程是cpu调度和执行的最小单位
- 一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
25. 守护线程是什么?
- 守护线程是运行在后台的一种特殊线程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
26. 创建线程有哪几种方式?
- 继承 Thread 重写 run 方法;
- 实现 Runnable 接口;
- 实现 Callable 接口。
27. 说一下 runnable 和 callable 有什么区别?
- runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
28. 线程有哪些状态?
线程的状态:
- NEW 尚未启动
- RUNNABLE 就绪态
- RUNNING 运行中
- BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
- WAITING 永久等待状态
- TIMED_WAITING 等待指定的时间重新被唤醒的状态
- TERMINATED 执行完成
29. sleep() 和 wait() 有什么区别?
- 类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
30. notify()和 notifyAll()有什么区别?
- notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
31. 线程的 run() 和 start() 有什么区别?
- start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
- 直接调用run方法,不会在线程中执行,只是相当于执行了一个普通的方法。