- 一、面向对象
- 1、static的作用
- 2、final的作用?在什么情况下使用呢?
- 3、抽象类和接口的区别?
- 4、接口里面常量的特点(jdk1.8之后)
- 5、Java语言有哪些特点?
- 6、Java与C++有什么关系,它们有什么区别?
- 7、JVM JRE JDK 三者关系
- 8、Oracle JDK 和 OpenJDK 的区别是什么?
- 9、Java有哪些数据类型?
- 10、switch能作用在什么数据类型上?
- 11、访问修饰符 public、protected、默认、private 的区别
- 12、break,continue,return 的区别和作用
- 13、final、finally、finalize的区别
- 14、面向对象和面向过程的区别?
- 15、讲讲面向对象的三大特性?
- 16、Java如何实现多态?
- 17、重写(Override)与重载(Overload)的区别?
- 18、Java创建对象的方式?
- 19、值传递和引用传递的区别?
- 20、== 与 equals 的区别?
- 21、介绍下hashCode()方法?
- 22、为什么要有hashCode?
- 23、hashCode()、equals() 的关系?
- 24、为什么重写equals方法必须重写hashCode方法?
- 25、元素排序Comparable和Comparator有什么区别?
- 26、this和super有什么区别?this能调用到父类吗?
- 27、方法重写时需要注意的问题有哪些?
- 28、为什么不同返回类型不算方法重载?
- 29、方法优先调用固定参数还是可选参数?
- 30、方法重写和方法重载有什么区别?
- 31、成员变量与局部变量的区别有哪些
- 二、集合
- 1、HashSet如何保证元素不重复?
- 2、在使用HashMap的时候,用String类型做key有什么好处?
- 3、HashMap有几种遍历方法?推荐使用哪种?
- 4、为什么HashMap的容量是2的倍数呢?
- 5、为什么HashMap会产生死循环?
- 6、为什么ConcurrentHashMap是线程安全的?
- 7、为什么ConcurrentHashMap不允许插入null值?
- 8、说说有哪些常见集合?
- 9、ArrayList和LinkedList有什么区别?
- 10、ArrayList的扩容机制了解吗?
- 11、ArrayList怎么序列化的知道吗?为什么用transient修饰数组?
- 12、快速失败(fail-fast)和安全失败(fail-safe)了解吗?
- 13、有哪几种实现ArrayList线程安全的方法?
- 14、CopyOnWriteArrayList了解多少?
- 15、能说一下HashMap的数据结构吗?
- 16、你对红黑树了解多少?为什么不用二叉树/平衡树呢?
- 17、红黑树怎么保持平衡的知道吗?
- 18、HashMap的put流程知道吗?
- 19、HashMap怎么查找元素的呢?
- 20、HashMap的哈希/扰动函数是怎么设计的?
- 21、你对Map了解多少?讲讲它们的数据结构?
- 22、讲讲当new一个HashMap的时候,会发生什么吗?
- 23、HashMap在put元素时,传递的key怎么计算哈希值?
- 24、简单说说HashMap的put/get方法的实现?
- 25、在HashMap中怎么判断一个元素是否相同呢?
- 26、HashMap的数据结构什么情况下才会用红黑树?
- 27、HashMap 为什么线程不安全?
- 28、简单讲讲ConCurrentHashMap?
- 29、HashMap 和 HashTable 的区别
- 30、Arrays.asList 获得的 List 应该注意什么
- 31、Collection 和 Collections 的区别
- 32、ArrayList、LinkedList 和 Vector 的区别
- 33、ArrayList和LinkedList有什么区别?
- 34、HashMap扩容机制
- 35、集合与数组的区别
- 36、集合框架有哪些优势
- 37、集合的底层数据结构
- 38、Itertator 是什么
- 39、介绍下 CopyOnWriteArrayList?
- 40、BlockingQueue 是什么?
- 41、在 Queue 中 poll()和 remove()有什么区别?
- 42、HashMap底层实现
- 43、HashMap 的 put 方法的具体流程?
- 44、如果初始化HashMap,传一个17的值new HashMap<>,它会怎么处理?
- 45、HashMap 内部节点是有序的吗?
- 三、常用类
- 1、Object类有哪些方法?
- 2、clone方法的使用
- 3、String,StringBuilder,StringBuffer 三者的区别?
- 4、String类为什么设计成不可变的?
- 5、字符型常量和字符串常量的区别?
- 6、
String str = "aaa";
与String str = new String("aaa");
一样吗?String str = new String("aaa");
创建了几个字符串对象? - 7、String类有哪些特性?
- 8、为什么会有包装类?int 与 Integer 的区别?
- 9、Integer缓存机制
- 10、JDK9为何要将String的底层实现由char[]改成了byte[]?
- 四、反射
- 五、IO流
- 六、异常处理
- 七、注解
- 八、泛型
- 九、Java新特性
一、面向对象
1、static的作用
特点:随着类的加载而加载;优先于对象存在;static修饰的成员,可被所有对象共享;在访问权限允许的情况下可被类直接调用
(1)修饰属性(称为类变量或静态变量)
- 多个对象共享一个静态变量,当通过某一个对象修改静态变量时,其他对象调用静态变量时是修改的
- 在访问权限允许的情况下,可使用“类.类变量”的方式调用
- 类变量与实例变量的对比 | | 类变量 | 实例变量 | | —- | —- | —- | | 类 | √ | × | | 对象 | √ | √ |
(2)修饰方法(称为静态方法)
- 在访问权限允许的情况下,可使用“类.静态方法”的方式调用
- 静态方法与非静态方法的对比 | | 静态方法 | 非静态方法 | | —- | —- | —- | | 类 | √ | × | | 对象 | √ | √ |
静态方法中只能调用静态变量或方法,非静态方法既可以调用非静态的方法或属性,也可以调用静态的方法或属性
(3)修饰代码块(称为静态代码块)
- 只能在JVM加载类时被执行一次
- 不需要程序主动调用,在JVM加载类时系统会执行static代码块
- 有多个static代码块,按顺序执行
(4)修饰内部类(称为静态内部类)
不能访问其外部类的实例成员(包括普通的成员变量和方法),只能访问外部类的类成员(包括静态成员变量和静态方法)
2、final的作用?在什么情况下使用呢?
(1)final修饰类
被修饰的类不能被其他类继承(不能有子类)
- 比如String类、Math类
(2)final修饰方法
被修饰的方法不能被子类重写
- 比如Object类中的getClass()
(3)final修饰变量
被修饰的变量,表示一旦被赋初值就不可被改变(不可变的是指变量的引用不可变而非引用指对象的内容不可变)
- 包括成员变量或局部变量,即称为常量,一般名称为大写,且只能被赋值一次
- 修饰成员变量时,必须显式指定初始值,不然没有意义(如果不指定系统会默认分配)
- 修饰局部变量时,一旦赋值以后,就只能在方法体内使用此形参,不能进行重新赋值
- static final修饰的变量即称为全局变量
(4)修饰参数
被修饰的参数,表示此参数在整个方法内不允许被修改
3、抽象类和接口的区别?
(1)接口
① 接口是 Java 中的一个抽象类型,用于定义对象的公共行为
② 定义接口使用的关键字是 interface,实现接口要使用 implements 关键字
③ 在接口中可以定义方法和常量,其普通方法是不能有具体的代码实现
④ JDK 8 之后,接口中可以创建 static 和 default 方法了,并且这两种方法可以有具体的代码实现;子类可以不重写接口中的 static 和 default 方法,不重写的情况下,默认调用的是接口的方法实现
⑤ 接口不能直接实例化
⑥ 接口中定义的变量默认为 public static final 类型
(2)抽象类
① 定义抽象类使用 abstract,子类用 extends 关键字继承父类
② 抽象类中可以包含普通方法和抽象方法,抽象方法不能有具体的代码实现。
③ 抽象类不能直接实例化。
④ 抽象类中属性修饰符无限制,可以定义 private 类型的属性。
(3)区别
① 定义的关键字不同,继承或实现的关键字不同
② 子类扩展的数量不同
③ 属性访问控制符不同
接口中属性的访问控制符只能是 public,可以省略不写(默认是 public static final 修饰的)
抽象类中的属性访问控制符无限制
④ 方法的访问控制符不同
接口中方法的只能是 public(默认)
抽象类中的方法控制符无限制,其中抽象方法不能使用 private 修饰
⑤ 方法实现不同
接口中普通方法不能有具体的方法实现,在 JDK 8 之后 static 和 default 方法必须有具体的代码实现
抽象类中普通方法可以有方法实现,抽象方法不能有方法实现
⑥ 静态代码块使用不同
接口中不能使用静态代码块
抽象类中可以使用静态代码块
4、接口里面常量的特点(jdk1.8之后)
(1)使用 public static final 修饰,可以省略不写
(2)接口中的常量是不可改变的,接口中的常量一旦定义必须赋值,否则没有意义
(3)只能通过“接口名.常量”的方式调用
5、Java语言有哪些特点?
(1)简单
(2)平台无关性
(3)安全:编译后会将所有的代码转换为字节码,人类无法直接读取
(4)动态性
(5)分布式:提供的功能有助于创建分布式应用,使用远程方法调用(RMI),程序可以通过网络调用另一个程序的方法并获取输出。
(6)健壮性:Java有强大的内存管理功能,在编译和运行时检查代码
(7)高性能
(8)编译与解释并存:Java 被编译成字节码,由 Java 运行时环境解释。
(9)多线程
6、Java与C++有什么关系,它们有什么区别?
(1)都支持面向对象
(2)C++ 支持指针,而 Java 没有指针的概念
(3)C++ 支持多继承,而 Java 不支持多重继承,但允许一个类实现多个接口
(4)Java 自动进行无用内存回收操作,不再需要进行手动删除,而 C++ 中必须由程序员释放内存资源
(5)Java 不支持操作符重载,操作符重载是 C++ 的突出特征;
(6)C 和 C++ 不支持字符串变量,在 Java 中字符串是用类对象(如String)实现
7、JVM JRE JDK 三者关系
8、Oracle JDK 和 OpenJDK 的区别是什么?
(1)Oracle JDK 版本每三年发布一次,而 OpenJDK 版本每三个月发布一次;
(2)OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是OpenJDK 的一个实现,并不是完全
开源的;
(3)Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多
的类和一些错误修复。如果您想开发企业/商业软件,建议选择 Oracle JDK。
(4)在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能
(5)Oracle JDK 不会为即将发布的版本提供长期支持
(6)Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPLv2 许可获得许可
9、Java有哪些数据类型?
10、switch能作用在什么数据类型上?
Java5以前: switch(expr)中,expr 只能是 byte、short、char、int
从Java5开始:expr 可以是 enum 类型。
从Java7开始:expr可以是String类型
11、访问修饰符 public、protected、默认、private 的区别
public : 对所有类可见。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。不能修饰外部类,但可以修饰内部类。
默认(不使用修饰符):在同一包内可见。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。不能修饰外部类,但可以修饰内部类。
12、break,continue,return 的区别和作用
break:跳出总上一层循环,不再执行循环(结束当前循环)
continue:跳出本次循环,继续执行下次循环(结束当次循环)
return:程序返回,不再执行下面的代码(结束当前方法,直接返回)
13、final、finally、finalize的区别
(1)final
① final修饰类:表示该类不能被其他类继承(不能有子类)
② final修饰方法:表示该方法不能被子类重写
③ final修饰变量:表示该变量一旦被赋初值就不可被改变(不可变的是指变量的引用不可变而非引用指对象的内容不可变)
④ final修饰参数:表示此参数在整个方法内不允许被修改
(2)finally
作为异常处理的一部分,它只能在 try/catch 语句中,表示这段语句最终一定被执行(无论是否抛出异常)
正常情况下,finally中的语句是一定执行的,但也有特殊情况:在try中终止虚拟机,退出Java程序
public class FinallyTest
{
public static void main(String[] args)
{
try
{
System.out.println("try中语句执行");
//终止虚拟机,退出Java程序
System.exit(0);
}
finally
{
System.out.println("finally中语句执行");
}
}
}
(3)finalize
① 在 java.lang.Object 里定义的方法,保证对象在被垃圾收集前完成特定资源的回收,但在 JDK 9 中已经被标记为弃用的方法
② 一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用
finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会
再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
14、面向对象和面向过程的区别?
(1)面向对象
①优点:易维护、易复用、易扩展;可以设计出低耦合的系统,使系统更加灵活。
②缺点:性能比面向过程低
(2)面向过程
①优点:性能比面向对象高
②缺点:没有面向对象易维护、易复用、易扩展
15、讲讲面向对象的三大特性?
(1)封装性
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信类或对象的进行信息隐藏。
(2)继承性
可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
(3)多态性
指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
16、Java如何实现多态?
(1)编译时多态
编译时多态在编译时就已经确定,运行的时候调用的是确定的方法(比如重载)
(2)运行时多态
①通常说的多态是运行时多态,编译时不确定调用哪个具体方法,一直延迟到运行时才能确定
②实现多态的必要条件:继承、重写、向上转型
- 继承:必须存在有继承关系的子类和父类
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样才既能可以调用父类的方法,又能调用子类的方法。
17、重写(Override)与重载(Overload)的区别?
(1)重写
重写发生在子类与父类之间,重写方法返回值和形参都不能改变,与方法返回值和访问修饰符无关。
(2)重载
重载是在一个类里面,方法名字相同,而形参不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的是构造器的重载。18、Java创建对象的方式?
new创建新对象、通过反射机制、采用clone机制、通过序列化机制
19、值传递和引用传递的区别?
(1)值传递
指在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
(2)引用传递
指在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用,即同一个内存空间
(3)说明
Java中只有值传递
①基本类型作为参数被传递时是值传递;
②引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。20、== 与 equals 的区别?
(1)==
常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较
①如果比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;
②如果比较的两个对象,那么比较的是两个对象的引用,即判断两个对象是否指向了同一块内存区域
(2)equals
判断两个对象是否相等
①当类没有覆盖equals()方法,则通过equals()比较该类的两个对象时,等价于通过==比较这两个对象;
②当类覆盖了equals()方法,一般,我们都覆盖equals()方法来比较两个对象的内容相等;若它们的内容相等,则返回 true。21、介绍下hashCode()方法?
hashCode() 的作用是获取哈希码,返回一个int整数。
哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object.java中。22、为什么要有hashCode?
以 “HashSet如何检查重复”为例子来说明为什么要有 hashCode
当把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时会与其他已经加入的对象的 hashCode 值作比较,
如果没有相符的 hashCode,HashSet会假设对象没有重复出现;
如果发现有相同 hashCode 值的对象,这时会调用 equals()方法来检查 hashCode 相等的对象是否真的相同;
如果两者相同,HashSet 就不会让其加入操作成功;
如果不同的话,就会重新散列到其他位置。
这样我们就大大减少 equals 的使用次数,相应就大大提高执行速度。23、hashCode()、equals() 的关系?
规范:
①若重写 equals() 方法,有必要重写 hashcode()方法,确保通过 equals() 方法判断结果为 true 的两个对象具备相等的 hashcode() 方法返回值;
②如果 equals() 方法返回 false,并不要求对这两个对象调用 hashCode() 方法得到两个不相同的数,有可能两个对象的哈希值是相同的24、为什么重写equals方法必须重写hashCode方法?
(1)hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式可以提高对象比较的效率。
(2)如果在重写 equals 时,不重写 hashCode,就会导致在某些场景下,就会出现程序执行的异常。举个例子,将两个相等的自定义对象存储在 Set 集合时,默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false,这表示两个对象不相等,于是在Set集合中插入两个判断不相等但实际上是相同的对象。Set集合去重的特点被破坏,显然就不太对。
(3)但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。
(4)那么为了保证程序的正常执行,所以我们就需要在重写 equals 时,也一并重写 hashCode 方法才行。25、元素排序Comparable和Comparator有什么区别?
(1)用法不同
① Comparable
通过重写 compareTo 方法实现排序;必须由自定义类内部实现排序方法Comparable 接口只有一个方法 compareTo,实现 Comparable 接口并重写 compareTo 方法就可以实现某个类的排序了,支持 Collections.sort 和 Arrays.sort 的排序
public class CompareTest
{
public static void main(String[] args)
{
Person p1 = new Person(1,"张三",18);
Person p2 = new Person(2,"李四",22);
Person p3 = new Person(3,"王五",25);
List<Person> list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
Collections.sort(list);
//输出
list.forEach(p -> System.out.println(p.getName() + ":" + p.getAge()));
}
}
class Person implements Comparable<Person>
{
private int id;
private String name;
private int age;
public Person(){}
public Person(int id, String name, int age)
{
this.id = id;
this.name = name;
this.age = age;
}
public int getId(){ return id; }
public void setId(int id){ this.id = id; }
public String getName(){ return name; }
public void setName(String name){ this.name = name; }
public int getAge(){ return age; }
public void setAge(int age){ this.age = age; }
@Override
public String toString()
{
return "Person{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}';
}
@Override
public int compareTo(Person person)
{
//按年龄从大到小排序
return person.age - this.age;
}
}
compareTo 方法接收的参数 p 是要对比的对象,排序规则是用当前对象和要对比的对象进行比较,然后返回一个 int 类型的值。正序从小到大的排序规则是:使用当前的对象值减去要对比对象的值;而倒序从大到小的排序规则刚好相反:是用对比对象的值减去当前对象的值。 注意:如果自定义对象没有实现 Comparable 接口,那么它是不能使用 Collections.sort 方法进行排序的,程序会报错。
② Comparator
通过重写 compare 方法实现排序;外部定义并实现排序方法
public class CompareTest
{
public static void main(String[] args)
{
Person p1 = new Person(1,"张三",18);
Person p2 = new Person(2,"李四",22);
Person p3 = new Person(3,"王五",25);
List<Person> list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
Collections.sort(list,new PersonComparator());
list.forEach(p -> System.out.println(p.getName() + ":" + p.getAge()));
}
}
class PersonComparator implements Comparator<Person>
{
@Override
public int compare(Person p1, Person p2)
{
//按年龄从大到小排序
return p2.getAge() - p1.getAge();
}
}
class Person
{
private int id;
private String name;
private int age;
public Person(){}
public Person(int id, String name, int age)
{
this.id = id;
this.name = name;
this.age = age;
}
public int getId(){ return id; }
public void setId(int id){ this.id = id; }
public String getName(){ return name; }
public void setName(String name){ this.name = name; }
public int getAge(){ return age; }
public void setAge(int age){ this.age = age; }
@Override
public String toString()
{
return "Person{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}';
}
}
Comparator匿名类
public class CompareTest
{
public static void main(String[] args)
{
Person p1 = new Person(1,"张三",18);
Person p2 = new Person(2,"李四",22);
Person p3 = new Person(3,"王五",25);
List<Person> list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
list.sort(new Comparator<Person>()
{
@Override
public int compare(Person p1, Person p2)
{
//按年龄从大到小排序
return p2.getAge() - p1.getAge();
}
});
list.forEach(p -> System.out.println(p.getName() + ":" + p.getAge()));
}
}
class Person
{
private int id;
private String name;
private int age;
public Person(){}
public Person(int id, String name, int age)
{
this.id = id;
this.name = name;
this.age = age;
}
public int getId(){ return id; }
public void setId(int id){ this.id = id; }
public String getName(){ return name; }
public void setName(String name){ this.name = name; }
public int getAge(){ return age; }
public void setAge(int age){ this.age = age; }
@Override
public String toString()
{
return "Person{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}';
}
}
(2)使用场景不同
① Comparable 必须要修改原有的类,在原有的类实现排序方法,像是“对内”进行排序的接口
② Comparator 无需修改原有类,通过创建新的自定义比较器 Comparator,来实现对原有类 Person 的排序功能,实现和原有类的解耦,可以看作是“对外”提供排序的接口
26、this和super有什么区别?this能调用到父类吗?
https://mp.weixin.qq.com/s/-eXL-Y6DHC_dX65PNQTq6w
(1)this和super的区别
① 指代的对象不同
super 指代的是父类,是用来访问父类的;而 this 指代的是当前类
② 查找范围不同
super 只能查找父类;而 this 会先从本类中找,如果找不到则会去父类中找
③ 是否能为本类属性赋值
this 可以用来为本类的实例属性赋值;而 super 则不能实现此功能
④ 是否可用于synchronized
this 可用于 synchronized(this){….} 加锁;而 super 则不能实现此功能
(2)this能调用到父类吗
可以,this 可以访问父类方法
public class ThisTest
{
public static void main(String[] args)
{
Son s = new Son();
s.sm();
}
}
class Father
{
public void fm()
{
System.out.println("父类中的方法fm()");
}
}
class Son extends Father
{
public void sm()
{
System.out.println("调用子类的sm()方法访问父类方法");
this.fm();
}
}
27、方法重写时需要注意的问题有哪些?
(1)重写的方法权限控制符不能变小,它可以等于或大于父类的权限控制符。
(2)子类返回值类型只能变小(主要指数值类型的范围大小)
(3)抛出的异常类型只能变小
(4)父类与子类的方法名必须保持一致
(5)父类与子类方法的参数类型和个数必须保持一致
28、为什么不同返回类型不算方法重载?
方法重载是指在同一个类中,定义了多个同名方法,但每个方法的参数类型或者是参数个数不同就是方法重载。
(1)为什么不同返回类型不算方法重载
方法名称 + 参数类型 + 参数个数 组成的一个唯一值,JVM就是通过这个方法签名来决定调用哪个方法的,由于方法的返回类型不是方法签名的组成部分,所以当同一个类中出现了多个方法名和参数相同,但返回值类型不同的方法时,JVM 就没办法通过方法签名来判断到底要调用哪个方法了。
(2)方法重载匹配原则
https://mp.weixin.qq.com/s/4pi1OZx8So6GjHD6yxjB3Q
①方法重载会优先调用和方法参数类型一模一样的方法
②基本类型自动转换成更大的基本类型
③自动装/拆箱匹配
④按照继承路线依次向上匹配
⑤可变参数匹配
29、方法优先调用固定参数还是可选参数?
优先调用固定参数,而非可选参数(可选参数的调用优先级是最低的)
30、方法重写和方法重载有什么区别?
31、成员变量与局部变量的区别有哪些
(1)作用域
成员变量:针对整个类有效。
局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)
(2)存储位置
成员变量:存储在堆内存中。
局部变量:存储在栈内存中。
(3)生命周期
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:当方法调用完,或者语句结束后,就自动释放。
(4)初始值
成员变量:有默认初始值。
局部变量:没有默认初始值,使用前必须赋值。
二、集合
1、HashSet如何保证元素不重复?
https://mp.weixin.qq.com/s/ASknNKns4nDPGhWxqaEEvA
(1)概述
从 HashSet 添加元素的执行流程来看:
当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现,会将对象插入到相应的位置中。但是如果发现有相同 hashcode 值的对象,这时会调用对象的 equals() 方法来检查对象是否真的相同,如果相同,则 HashSet 就不会让重复的对象加入到 HashSet 中,这样就保证了元素的不重复。
(2)从源码层面来看(基于JDK8)
当将一个键值对放入 HashMap 时,首先根据 key 的 hashCode() 返回值决定该 Entry 的存储位置。如果有两个 key 的 hash 值相同,则会判断这两个元素 key 的 equals() 是否相同,如果相同就返回 true,说明是重复键值对,那么 HashSet 中 add() 方法的返回值会是 false,表示 HashSet 添加元素失败。
因此,如果向 HashSet 中添加一个已经存在的元素,新添加的集合元素不会覆盖已有元素,从而保证了元素的不重复。如果不是重复元素,put 方法最终会返回 null,传递到 HashSet 的 add 方法就是添加成功。
2、在使用HashMap的时候,用String类型做key有什么好处?
HashMap 内部实现是通过 key 的 hashCode 来确定 value 的存储位置,因为String类是不可变的,所以
当创建字符串时,它的 hashCode 被缓存下来,不需要再次计算,所以相比于其他类型要更快。
3、HashMap有几种遍历方法?推荐使用哪种?
(1)JDK 8 之前的遍历
① EntrySet
public class TraverseHashMapTest
{
HashMap<String,String> map = new HashMap<>(){{
put("Java","Java Value");
put("Mysql","Mysql Value");
put("Redis","Redis Value");
}};
@Test
public void testEntrySet()
{
for(Map.Entry<String,String> entry : map.entrySet())
{
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
② KeySet(不推荐使用)
循环 Key 的内容,再通过 map.get(key) 获取 Value 的值
public class TraverseHashMapTest
{
@Test
public void testKeySet()
{
for(String key : map.keySet())
{
System.out.println(key + ":" + map.get(key));
}
}
}
注:KeySet 循环了两遍集合,第一遍循环是循环 Key,而获取 Value 有需要使用 map.get(key),相当于有循环了一遍集合,所以 KeySet 循环不能建议使用,因为循环了两次,效率比较低。
③ EntrySet 迭代器
public class TraverseHashMapTest
{
@Test
public void testIteratorEntry()
{
Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
while(iterator.hasNext())
{
Map.Entry<String,String> entry = iterator.next();
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
④ KeySet 迭代器
public class TraverseHashMapTest
{
@Test
public void testIteratorKey()
{
Iterator<String> iterator = map.keySet().iterator();
while(iterator.hasNext())
{
String key = iterator.next();
System.out.println(key + ":" + map.get(key));
}
}
}
迭代器的作用:使用迭代器的优点是可以在循环的时候,动态的删除集合中的元素。而非迭代器的方式则不能在循环的过程中删除元素(程序会报错)。
public class TraverseHashMapTest
{
//不使用迭代器删除集合中的元素(会报错)
@Test
public void testDeleteFor()
{
for(Map.Entry<String,String> entry : map.entrySet())
{
if("Java".equals(entry.getKey()))
{
map.remove(entry.getKey());
continue;
}
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
//使用迭代器删除集合中的元素(可以成功删除)
@Test
public void testDeleteIterator()
{
Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
while(iterator.hasNext())
{
Map.Entry<String,String> entry = iterator.next();
if("Java".equals(entry.getKey()))
{
iterator.remove();
continue;
}
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
(2)JDK 8 之后的遍历
① Lambda表达式
public class TraverseHashMapTest
{
@Test
public void testLambda()
{
HashMap<String,String> map1 = new HashMap<>();
map1.put("BlackPink1","Jisoo");
map1.put("BlackPink2","Jennie");
map1.put("BlackPink3","Rose");
map1.put("BlackPink4","Lisa");
map1.forEach((key,value) -> {
System.out.println(key + ":" + value);
});
}
}
② Stream单线程遍历
public class TraverseHashMapTest
{
@Test
public void testStream1()
{
HashMap<String,String> map2 = new HashMap<>();
map2.put("BlackPink1","Jisoo");
map2.put("BlackPink2","Jennie");
map2.put("BlackPink3","Rose");
map2.put("BlackPink4","Lisa");
map2.entrySet().stream().forEach((entry) -> {
System.out.println(entry.getKey() + ":" + entry.getValue());
});
}
}
③ Stream多线程遍历
public class TraverseHashMapTest
{
@Test
public void testStream2()
{
HashMap<String,String> map3 = new HashMap<>();
map3.put("BlackPink1","Jisoo");
map3.put("BlackPink2","Jennie");
map3.put("BlackPink3","Rose");
map3.put("BlackPink4","Lisa");
map3.entrySet().stream().parallel().forEach((entry) -> {
System.out.println(entry.getKey() + ":" + entry.getValue());
});
}
}
4、为什么HashMap的容量是2的倍数呢?
(1)为了方便哈希取余
将元素放在table数组上面,是用hash值%数组大小定位位置,而HashMap是用hash值&(数组大小-1),能和前面达到一样的效果
HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111…111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。
(2)在扩容时,利用扩容后的大小也是2的倍数,将已经产生hash碰撞的元素完美的转移到新的table中去
5、为什么HashMap会产生死循环?
死循环问题发生在 JDK 1.7 版本中,造成这个问题主要是 HashMap 自身的运行机制,加上并发操作,从而导致了死循环。
(1)死循环的发生
①第一步:因为并发 HashMap 扩容导致的,并发扩容的第一步,线程 T1 和线程 T2 要对 HashMap 进行扩容操作,此时 T1 和 T2 指向的是链表的头结点元素 A,而 T1 和 T2 的下一个节点,也就是 T1.next 和 T2.next 指向的是 B 节点
②第二步:线程 T2 时间片用完进入休眠状态,而线程 T1 开始执行扩容操作,一直到线程 T1 扩容完成后,线程 T2 才被唤醒(采用头插法,HashMap顺序发生改变,但线程 T2 对于发生的一切是不可知的,所以它的指向元素依然没变)
③第三步:当线程 T1 执行完,而线程 T2 恢复执行时,死循环就建立了:因为 T1 执行完扩容之后 B 节点的下一个节点是 A,而 T2 线程指向的首节点是 A,第二个节点是 B,这个顺序刚好和 T1 扩完容完之后的节点顺序是相反的。T1 执行完之后的顺序是 B 到 A,而 T2 的顺序是 A 到 B,这样 A 节点和 B 节点就形成死循环了,这就是 HashMap 死循环导致的原因。
(2)解决方案
①使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案)
②使用线程安全容器 Hashtable 替代(性能低,不建议使用)
③使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)
6、为什么ConcurrentHashMap是线程安全的?
(1)JDK 1.7 底层实现
使用的是数组加链表的形式实现的,数组分为:大数组 Segment 和小数组 HashEntry。一个大数组Segment 中有很多个 HashEntry,每个 HashEntry 中又有多条数据,这些数据用链表连接。
线程安全的实现(以添加元素的put方法为例)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往该 Segment 写入前,先确保获取到锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
// Segment 内部数组
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有值...
}
else {
// 放置 HashEntry 到特定位置,如果超过阈值则进行 rehash
// 忽略其他代码...
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
Segment 本身是基于 ReentrantLock 实现的加锁和释放锁的操作,这样就能保证多个线程同时访问 ConcurrentHashMap 时,同一时间只有一个线程能操作相应的节点,这样就保证了 ConcurrentHashMap 的线程安全(在 Segment 加锁,称为分段锁或片段锁)
(2)JDK 1.8 底层实现
底层使用 数组 + 链表/红黑树 实现
线程安全实现(以添加元素的put方法为例)
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 节点为空
// 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 细粒度的同步修改操作...
}
}
// 如果超过阈值,升级为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronize 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了(简化:ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高)
7、为什么ConcurrentHashMap不允许插入null值?
主要是为了防止并发场景下的歧义问题
假设 ConcurrentHashMap 允许插入 null,那么此时就会有二义性问题(含义不清或不明确):值没有在集合中,所以返回 null;值就是 null,所以返回的就是它原本的 null 值。
在 Java 语言中,HashMap 这种单线程下使用的集合是可以设置 null 值的,而并发集合如 ConcurrentHashMap 或 Hashtable 是不允许给 key 或 value 设置 null 值的,这是 JDK 源码层面直接实现的
8、说说有哪些常见集合?
(1)Collection是集合List、Set的父接口,它主要有两个子接口:
① List:元素有序,元素可重复,可以插入多个null值,元素有索引,常用实现类有ArrayList、LinkedList 和 Vector。
② Set:元素无序,元素不可重复(必须保证元素唯一性),只允许插入一个null值,常用实现类有HashSet、LinkedHashSet 以及 TreeSet。
(2)Map
键值对集合,存储键、值和之间的映射。Key 无序,唯一;value 不要求有序,允许重复。
常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
9、ArrayList和LinkedList有什么区别?
(1)数据结构不同
①ArrayList基于数组实现
②LinkedList基于双向链表实现
(2)多数情况下,ArrayList更利于查找,LinkedList更利于增删
①ArrayList基于数组实现,get(int index)可以直接通过数组下标获取,时间复杂度是O(1);LinkedList基于链表实现,get(int index)需要遍历链表,时间复杂度是O(n)
②ArrayList增删如果是数组末尾的位置,直接插入或者删除就可以了,但是如果插入中间的位置,就需要把插入位置后的元素都向前或者向后移动,甚至还有可能触发扩容;LinkedList的插入和删除只需要改变前驱节点、后继节点和插入节点的指向就行了,不需要移动元素。
(3)是否支持随机访问
①ArrayList基于数组,可以根据下标查找,支持随机访问(实现了RandmoAccess 接口,这个接口只是用来标识是否支持随机访问)
②LinkedList基于链表,所以它没法根据序号直接获取元素,它没有实现RandmoAccess 接口,标记不支持随机访问
(4)内存占用情况不同
①ArrayList基于数组,是一块连续的内存空间,可能会有空的内存空间,存在一定空间浪费
②LinkedList基于链表,内存空间不连续,每个节点需要存储前驱和后继,所以每个节点会占用更多的空间
10、ArrayList的扩容机制了解吗?
ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。
ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过。
11、ArrayList怎么序列化的知道吗?为什么用transient修饰数组?
ArrayList使用transient
修饰存储元素的elementData
的数组,transient
关键字的作用是让被修饰的成员属性不被序列化。
(1)为什么ArrayList不直接序列化元素数组呢?
这样可以提高序列化和反序列化的效率,还可以节省内存空间。
(2)ArrayList怎么序列化呢?
ArrayList通过两个方法readObject
、writeObject
自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStream
和ObjectInputStream
来进行序列化和反序列化。
12、快速失败(fail-fast)和安全失败(fail-safe)了解吗?
(1)快速失败(fail-fast)
Java集合的一种错误检测机制
①定义:在用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了增删改,则会抛出Concurrent Modification Exception
②原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount
变量。集合在被遍历期间如果内容发生变化,就会改变modCount
的值。每当迭代器使用hashNext()/next() 遍历下一个元素之前,都会检测modCount
变量是否为expectedmodCount
值,是的话就返回遍历;否则抛出异常,终止遍历。
③注意:这里异常的抛出条件是检测到modCount!=expectedmodCount
这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。
④场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(比如在迭代过程中对数据的增删改),比如ArrayList
类
(2)安全失败(fail—safe)
①特点:在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
②原理:在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception
③场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList
类。
13、有哪几种实现ArrayList线程安全的方法?
(1)使用 Vector 代替 ArrayList。(不推荐,Vector是一个历史遗留类)
(2)使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list。
(3)使用 CopyOnWriteArrayList 代替 ArrayList。
(4)在使用 ArrayList 时,应用程序通过同步机制去控制 ArrayList 的读写。
14、CopyOnWriteArrayList了解多少?
是线程安全版本的ArrayList
采用了一种读写分离的并发策略。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
15、能说一下HashMap的数据结构吗?
JDK1.7的数据结构是数组+链表
JDK1.8的数据结构是数组+链表+红黑树
桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。
- 数据元素通过散列函数,映射到桶数组对应索引的位置
- 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
- 如果链表长度>8&&数组大小>=64,链表转为红黑树
-
16、你对红黑树了解多少?为什么不用二叉树/平衡树呢?
红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:
每个节点要么是红色,要么是黑色;
- 根节点永远是黑色的;
- 所有的叶子节点都是是黑色的;
- 每个红色节点的两个子节点一定都是黑色;
- 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
(1)为什么不用二叉树
红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。
(2)为什么不用平衡二叉树
平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。
17、红黑树怎么保持平衡的知道吗?
18、HashMap的put流程知道吗?
(1)首先根据哈希函数(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
获取一个新的哈希值
(2)判断tab是否为空或者长度为0,如果是则进行扩容操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
(3)根据哈希值计算下标,如果对应小标正好没有存放数据,则直接插入即可否则需要覆盖
(4)判断tab[i]是否为树节点,否则向链表中插入数据,是则向树中插入节点
(5)如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树
(6)最后所有元素处理完成后,判断是否超过阈值,超过则扩容。
19、HashMap怎么查找元素的呢?
(1)使用哈希函数,获取新的哈希值
(2)计算数组下标,获取节点
(3)当前节点和key匹配,直接返回
(4)否则,当前节点是否为树节点,查找红黑树
(5)否则,遍历链表查找
20、HashMap的哈希/扰动函数是怎么设计的?
HashMap的哈希函数是先拿到 key 的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作,这么设计是为了降低哈希碰撞的概率。
static final int hash(Object key) {
int h;
// key的hashCode和key的hashCode右移16位做异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
21、你对Map了解多少?讲讲它们的数据结构?
(1)Map
Map在Java里边是一个接口,常见的实现类有HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap。
(2)数据结构
HashMap底层数据机构是数组+链表/红黑树
、LinkedHashMap底层数据结构是数组+链表+双向链表
、TreeMap底层数据结构是红黑树
,而ConcurrentHashMap底层数据结构也是数组+链表/红黑树
22、讲讲当new一个HashMap的时候,会发生什么吗?
(1)HashMap有几个构造方法,但最主要的就是指定初始值大小和负载因子的大小,如果我们不指定,默认HashMap的大小为16
,负载因子的大小为0.75
。
(2)HashMap的大小只能是2次幂的(因为只有大小为2次幂时,才能合理用位运算替代取模),假设你传一个10进去,实际上最终HashMap的大小是16,你传一个7进去,HashMap最终的大小是8,具体的实现在tableSizeFor可以看到。我们把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。在HashMap里用的是位运算来代替取模,能够更加高效地算出该元素所在的位置。
(3)负载因子的大小决定着哈希表的扩容和哈希冲突。比如现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个元素,一旦超过12个元素,则哈希表需要扩容。每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,如果有,则需要扩容,扩容的时候时候默认是扩原来的2倍。
23、HashMap在put元素时,传递的key怎么计算哈希值?
实现就在hash
方法上,可以发现的是,它是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。这样做的好处可以增加了随机性,减少了碰撞冲突的可能性。
24、简单说说HashMap的put/get方法的实现?
(1)put方法
首先对key做hash运算,计算出该key所在的index。如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。假设key是相同的,则替换到原来的值。最后判断哈希表是否满了(当前哈希表大小*负载因子),如果满了,则扩容。
(2)get方法
对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突,假设没有直接返回,假设有则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。
25、在HashMap中怎么判断一个元素是否相同呢?
首先会比较hash值,随后会用==运算符和equals()来判断该元素是否相同。
26、HashMap的数据结构什么情况下才会用红黑树?
当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树大小为6时,会退化为链表。
红黑树退化为链表的操作主要出于查询和插入时对性能的考量:链表查询时间复杂度O(N),插入时间复杂度O(1),红黑树查询和插入时间复杂度O(logN)
27、HashMap 为什么线程不安全?
https://mp.weixin.qq.com/s?__biz=MzkyMTI3Mjc2MQ==&mid=2247485906&idx=1&sn=64785914b2bc6c53b21d7c62fbb605a7&source=41#wechat_redirect
(1)多线程下扩容死循环
JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
(2)多线程的put可能导致元素的丢失
多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
(3)put和get并发时,可能导致get为null
线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。
28、简单讲讲ConCurrentHashMap?
(1)ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。
(2)ConcurrentHashMap的底层数据结构是数组+链表/红黑树,它能支持高并发的访问和更新,是线程安全的。
(3)ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步,在get的时候没有加锁,Node都用了volatile给修饰。
(4)在扩容时,会给每个线程分配对应的区间,并且为了防止putVal导致数据不一致,会给线程的所负责的区间加锁
(5)除了ConcurrentHashMap,Hashtable和使用Collections来包装出一个线程安全的Map均可实现线程安全,但他们都非常低效(外层直接套synchronized),所以我们一般有线程安全问题考量的,都使用ConcurrentHashMap。
29、HashMap 和 HashTable 的区别
(1)相同点
基于哈希表实现的,内部每个元素都是key-value
键值对,都实现了 Map、Cloneable、Serializable 接口。
(2)不同点
①父类不同:HashMap 继承了AbstractMap
类;而 HashTable 继承了Dictionary
类
②空值不同:HashMap 允许空的 key 和 value 值;HashTable 不允许空的 key 和 value 值
③线程安全性:HashMap不是线程安全的;HashTable是线程安全的
④性能方面:HashMap的put和get操作效率比HashTable要快
⑤初始容量不同:HashTable 的初始长度是11,之后每次扩充容量变为之前的 2n+1;HashMap 的初始长度为16,之后每次扩充变为原来的两倍
30、Arrays.asList 获得的 List 应该注意什么
Arrays.asList 转换完成后不能再进行任何 List 元素的增加或者删除的操作,但支持对元素的修改操作。
31、Collection 和 Collections 的区别
(1)相同点
都是位于java.util
包下的类
(2)不同点
①Collection是很多集合类的父接口
②Collections是集合类的工具类,提供了一些工具类的基本使用
32、ArrayList、LinkedList 和 Vector 的区别
(1)相同点
都位于java.util
包下的工具类,它们都实现了List
接口。
(2)不同点
①ArrayList:底层是动态数组;遍历访问非常快,但是增删比较慢;是非线程安全的容器;在扩容时会增加 50% 的容量。
②LinkedList:底层是双向链表;增加和删除操作非常快,遍历比较慢;是一个非线程安全的容器。
③Vector:最早出现的集合容器;是一个线程安全的容器,它的每个方法都加上了synchronized锁,所以它的增删、遍历效率都很低;扩容时容量会增加一倍。
33、ArrayList和LinkedList有什么区别?
(1)底层数据结构
ArrayList是数组,LinkedList是链表
(2)效率
随机访问的时候,ArrayList的效率比较高;插入、删除数据时,LinkedList的效率比较高。
(3)开销
LinkedList比ArrayList开销更大,因为LinkedList的节点除了存储数据,还需要存储引用。
34、HashMap扩容机制
(1)把数组长度变为原来的两倍,
(2)把旧数组的元素重新计算hash插入到新数组中,jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。
35、集合与数组的区别
数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
36、集合框架有哪些优势
容量自动增长扩容;
提供高性能的数据结构和算法;
可以方便地扩展或改写集合,提高代码复用性和可操作性;
通过使用 JDK 自带的集合类,可以降低代码维护和学习新 API 成本。
37、集合的底层数据结构
(1)List
ArrayList:Object 数组
Vector:Object 数组
LinkedList:双向循环链表
(2)Set
HashSet:基于HashMap实现,底层使用HashMap存储数据
LinkedHashSet:继承于HashSet,底层使用LinkedHashMap来保存所有元素
TreeSet:红黑树
(3)Map
HashMap:数组+链表(JDK1.8之前);数据+链表+红黑树(JDK1.8之后)
LinkedHashMap:继承于HashMap,底层结构为数据+链表+红黑树
HashTable:数组+链表
TreeMap:红黑树
38、Itertator 是什么
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。
List<String> list = new ArrayList<>();
...
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
如何边遍历边移除 Collection 中的元素?
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.next();
it.remove();
}
39、介绍下 CopyOnWriteArrayList?
CopyOnWriteArrayList 是 ArrayList 的线程安全版本,在读操作时不加锁,跟 ArrayList 类似;在写操作时,复制出一个新的数组,在新数组上进行操作,操作完了,将底层数组指针指向新数组。
适合使用在读多写少的场景。
40、BlockingQueue 是什么?
Java.util.concurrent.BlockingQueue
是一个队列,在进行检索或移除一个元素的时候,线程会等待队列变为非空;当在添加一个元素时,线程会等待队列中的可用空间。
主要用于实现生产者-消费者模式。
Java 提供了几种 BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
41、在 Queue 中 poll()和 remove()有什么区别?
相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
42、HashMap底层实现
43、HashMap 的 put 方法的具体流程?
当我们 put 的时候,首先计算 key 的 hash 值,这里调用了 hash 方法
①判断键值对数组 table[i]是否为空或为 null,否则执行 resize()进行扩容;
②根据键值 key 计算 hash 值得到插入的数组索引 i,如果 table[i]==null,直接新建节点添加,转向 ⑥,如果 table[i]不为空,转向 ③;
③判断 table[i]的首个元素是否和 key 一样,如果相同直接覆盖 value,否则转向 ④,这里的相同指的是 hashCode 以及 equals;
④判断 table[i] 是否为 treeNode,即 table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向 ⑤;
⑤遍历 table[i],判断链表长度是否大于 8,大于 8 的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现 key 已经存在直接覆盖 value 即可;
⑥插入成功后,判断实际存在的键值对数量 size 是否超多了最大容量 threshold,如果超过,进行扩容。
44、如果初始化HashMap,传一个17的值new HashMap<>,它会怎么处理?
初始化时,传的不是2的倍数时,HashMap会向上寻找离得最近的2的倍数,所以传入17,但HashMap的实际容量是32。
45、HashMap 内部节点是有序的吗?
HashMap是无序的,根据 hash 值随机插入。
如果想使用有序的Map,可以使用LinkedHashMap 或者 TreeMap。
46、
三、常用类
1、Object类有哪些方法?
https://www.runoob.com/java/java-object-class.html
2、clone方法的使用
protected Object clone():创建并返回一个对象的拷贝(浅拷贝,对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存。而深拷贝则会连引用的对象也重新创建)
- 说明
由于 Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常
举例
class RunoobTest implements Cloneable {
// 声明变量
String name;
int likes;
public static void main(String[] args) {
// 创建对象
RunoobTest obj1 = new RunoobTest();
// 初始化变量
obj1.name = "Runoob";
obj1.likes = 111;
// 打印输出
System.out.println(obj1.name); // Runoob
System.out.println(obj1.likes); // 111
try {
// 创建 obj1 的拷贝
RunoobTest obj2 = (RunoobTest) obj1.clone();
// 使用 obj2 输出变量
System.out.println(obj2.name); // Runoob
System.out.println(obj2.likes); // 111
} catch (Exception e) {
System.out.println(e);
}
}
}
3、String,StringBuilder,StringBuffer 三者的区别?
(1)可变与不可变
String对象是不可变的,对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去#String类用字符数组保存字符串,由于final修饰,所以String对象不可变
private final char value[];
StringBuilder与StringBuffer是可变的
#继承自AbstractStringBuilder类,在AbstractStringBuilder中使用字符数组保存字符串
char[] value;
(2)是否线程安全
String是线程安全的
StringBuilder不是线程安全的
StringBuffer是线程安全的(对方法加了同步锁或者对调用的方法加了同步锁)
(3)如果只在单线程中使用字符串缓冲区,那么StringBuilder的效率会更高些。4、String类为什么设计成不可变的?
从设计考虑,效率优化,安全性三大方面考虑
(1)便于实现字符串常量池(String pool)
①由于会大量的使用String类,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间,当初始化一个String类型变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
②如果String类是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现
(2)使多线程安全
对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全
(3)避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制
所需要的String参数,其不可变性可以保证连接的安全性。
(4)加快字符串处理速度
String是不可变的,保证了hashCode的唯一性,在创建对象时其hashCode可以放心缓存,不需要重新计算。这也就是Map将String作为Key的原因,处理速度要快过其它的键对象。5、字符型常量和字符串常量的区别?
(1)形式上:字符型常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符;
(2)含义上:字符常量相当于一个整型值(ASCII 值),可以参加表达式运算;字符串常量代表一个地址值,是该字符串在内存中存放位置,相当于对象;
(3)占内存大小:字符常量只占2个字节;字符串常量占若干个字节。6、
String str = "aaa";
与String str = new String("aaa");
一样吗?String str = new String("aaa");
创建了几个字符串对象?(1)
String a = "aaa";
程序运行时会在常量池中查找"aaa"
字符串,若没有,会将"aaa"
字符串放进常量池,再将其地址赋给a
;若有,将找到的"aaa"
字符串的地址赋给a
。
(2)String b = new String("aaa");
程序会在堆内存中开辟一片新空间存放新对象,同时会将"aaa"
字符串放入常量池,相当于创建了两个对象,无论常量池中有没有"aaa"
字符串,程序都会在堆内存中开辟一片新空间存放新对象。7、String类有哪些特性?
(1)不可变性
String是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
(2)常量池优化
String对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,直接返回缓存的引用
(3)使用final修饰
表示 String 类不能被继承,提高了系统的安全性。8、为什么会有包装类?int 与 Integer 的区别?
(1)为什么会有包装类
①使用Java时,很多情况下,需要以对象的形式操作,每个基本数据类型对应一个包装类
②包装类的存在解决了基本数据类型无法做到的事情,比如泛型类型参数、序列化、类型转换、高频区间数据缓存等问题。
(2)int 与 Integer 的区别
①数据类型不同:int 是基础数据类型,Integer 是包装数据类型;
②默认值不同:int 的默认值是 0,Integer 的默认值是 null;
③内存中存储的方式不同:int 在内存中直接存储的是数据值,而 Integer 实际存储的是对象引用,当 new 一个 Integer 时实际上是生成一个指针指向此对象;
④实例化方式不同:Integer 必须实例化才可以使用,而 int 不需要;
⑤变量的比较方式不同:int 可以使用 == 来对比两个变量是否相等,而 Integer 一定要使用 equals 来比较两个变量是否相等。9、Integer缓存机制
为了节省内存和提高性能,Integer类在内部通过使用相同的对象引用实现缓存和重用,Integer类默认在-128 ~ 127 之间,可以通过 -XX:AutoBoxCacheMax进行修改,且这种机制仅在自动装箱的时候有用,在使用构造器创建Integer对象时无用。
Integer a = 10;
Integer b = 10;
Integer c = 130;
Integer d = 130;
System.out.println(a == b); //true
System.out.println(c == d); //false
10、JDK9为何要将String的底层实现由char[]改成了byte[]?
目的:节省字符串占用的内存,进而减少GC的次数
https://mp.weixin.qq.com/s/Bh67Y_UDH9g-EZAVBApk-Q
四、反射
1、反射机制的应用场景有哪些?
(1)项目开发过程中很少直接使用反射
(2)模块化的开发,通过反射去调用对应的字节码
(3)动态代理设计模式采用了反射机制
(4)使用的 Spring 等框架也大量使用到了反射机制。
2、什么是反射机制?
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
(1)静态编译和动态编译
静态编译:在编译时确定类型,绑定对象
动态编译:运行时确定类型,绑定对象
(2)优缺点
①优点:运行期类型的判断,动态加载类,提高代码灵活度
②缺点:性能瓶颈(反射相当于一系列解释操作,通知JVM要做的事情)
3、Java获取反射的三种方法
(1)使用 Class.forName 静态方法。
(2)使用类的.class 方法
(3)使用实例对象的 getClass() 方法。
4、JDK动态代理和CGLIB动态代理的区别
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
5、静态代理和动态代理的区别
静态代理中代理类在编译期就已经确定,而动态代理则是JVM运行时动态生成,静态代理的效率相对动态代理来说相对高一些,但是静态代理代码冗余大,一旦需要修改接口,代理类和委托类都需要修改。
五、IO流
1、Java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
2、BIO,NIO,AIO 有什么区别?
(1)简答
BIO:Block IO,同步阻塞式 IO,平常使用的传统 IO,特点是模式简单使用方便,并发处理能力低。
NIO:Non IO,同步非阻塞 IO,传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO,异步非堵塞 IO,NIO 的升级,也叫 NIO2,异步 IO 的操作基于事件和回调机制。
(2)详细回答BIO (Blocking I/O)
:①同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成;②适用在活动连接数不是特别高(小于单机1000)的情况下。③模式简单,使用方便,并发处理能力低;④对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性。NIO (New I/O)
:①同步非阻塞的I/O模型;②Java 1.4 中引入;③支持面向缓冲的,基于通道的I/O操作方法;④提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式;⑤对于高负载、高并发的(网络)应用,使用 NIO 来开发AIO (Asynchronous I/O)
:①在 Java 7 中引入;②基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。③异步IO。
六、异常处理
1、Exception 和 Error 有什么区别
相同点:都继承了 Throwable 类
(1)Exception
异常Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
主要分为两种异常,一种是编译期出现的异常,称为 checkedException ,一种是程序运行期间出现的异常,称为 uncheckedException
(2)Error
指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复的,Error 不能被捕获。
2、Java 常见异常有哪些?
java.lang.IllegalAccessError
:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。java.lang.InstantiationError
:实例化错误。当一个应用试图通过 Java 的 new 操作符构造一个抽象类或者接口时抛出该异常.java.lang.OutOfMemoryError
:内存不足错误。当可用内存不足以让 Java 虚拟机分配给一个对象时抛出该错误。java.lang.StackOverflowError
:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。java.lang.ClassCastException
:类造型异常。假设有类 A 和 B(A 不是 B 的父类或子类),O 是 A 的实例,那么当强制将 O 构造为类 B 的实例时抛出该异常。该异常经常被称为强制类型转换异常。java.lang.ClassNotFoundException
:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历 CLASSPAH 之后找不到对应名称的 class 文件时,抛出该异常。java.lang.ArithmeticException
:算术条件异常。譬如:整数除零等。java.lang.ArrayIndexOutOfBoundsException
:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
七、注解
1、讲讲什么是注解?开发中有用到注解吗?
(1)什么是注解
注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。
(2)开发中使用注解
比如Spring、Mybatis等框架中的@Controller、@Select等注解;原生中有@Override、@Deprecated等注解用来标记和检查;还有一种元注解,用来修饰注解,常用的元注解有@Retention 和@Target:@Retention注解可以简单理解为设置注解的生命周期;而@Target表示这个注解可以修饰哪些地方(比如方法、还是成员变量、还是包等等)
八、泛型
1、Java 泛型了解么?
Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
2、常用的通配符有哪些?
? 表示不确定的 Java 类型
T (type) 表示具体的一个 Java 类型
K V (key value) 分别代表 Java 键值中的 Key Value
E (element) 代表 Element
3、什么是类型擦除?
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的类型信息都会被擦掉,也就是说,在运行的时候是没有泛型的。
九、Java新特性
1、JDK 1.8 之后有哪些新特性
(1)接口默认方法:Java8 允许我们给接口添加一个非抽象的方法实现,需要使用 default 关键字
(2)Lambda 表达式和函数式接口:Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),本质上是一段匿名内部类,也可以是一段可以传递的代码。
(3)StreamAPI:用函数式编程方式在集合类上进行复杂操作的工具,配合 Lambda 表达式可以方便的对集合进行处理。
(4)方法引用:方法引用提供了非常有用的语法,可以直接引用已有 Java 类或对象(实例)的方法或构造器。
(5)日期时间 API:Java8 引入了新的日期时间 API 改进了日期时间的管理。
(6)Optional 类:提供解决空指针异常的方式
(7)新的编译工具