2019Java面试题整理(1)_JavaSE部分

本文主要介绍了JavaSE的基础和高级知识点,包括Java基础(如equals()和hashCode()的关系、Map分类)、关键字(如synchronized、volatile)、面向对象(如重载与重写、访问控制修饰符)、集合(如HashMap和ArrayList的区别)、线程(如线程安全和线程池)等内容。通过对这些知识点的深入理解,可以帮助开发者更好地应对Java面试。
摘要由CSDN通过智能技术生成

JavaSE

一、Java基础

Java基础部分(基本语法、Java特性等)


1 为什么重写equals还要重写hashcode

​ 因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:

1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。

2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。

​ 所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性。

​ 然而hashCode()和equal()一样都是基本类Object里的方法,而和equal()一样,Object里hashCode()里面只是返回当前对象的地址,如果是这样的话,那么我们相同的一个类,new两个对象,由于他们在内存里的地址不同,则他们的hashCode()不同,所以这显然不是我们想要的,所以我们必须重写我们类的hashCode()方法,即一个类,在hashCode()里面返回唯一的一个hash值。


2 说一下map的分类和常见的情况

类名:java.util.Map,包含4个实现类:HashMap、Hashtable、LinkedHashMap、TreeMap

类名是否允许键或值为null是否线程安全是否有序
HashMap
Hashtable
LinkedHashMap
TreeMap
  • HashMap是最常用的Map,它根据键的hashcode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap仅允许一条记录的键为null,允许多条记录的值为null,不支持线程同步,如需要则可用Collections的synchronizeMap方法使HashMap具有同步能力
  • Hashtable与HashMap类似,不同之处在于它不允许记录的键或值为null,它还支持线程同步,但也因此导致Hashtable在写入时比较慢
  • LinkedHashMap保存了记录的插入顺序,用Iterator遍历时,先得到的记录是先插入的,遍历时比HashMap慢,但有特殊情况:当HashMap容量很大,但实际数据很少时遍历起来可能比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和其容量有关
  • TreeMap实现了SortMap接口,能够把它保存的记录根据键值默认升序,也可以指定排序的比较器。当用Iterator遍历TreeMap时,得到的记录是排过序的

3 Object若不重写hashCode()的话,hashCode是如何计算出来的

​ Object的hashCode()是本地方法,其直接返回对象的内存地址作为hashCode


4 “==”比较的是什么,与equals()的区别

​ “= =”比较的是两个对象的内存引用,如果两个对象的引用完全相同(即指向同一个对象)时,“= =”操作返回true,否则返回false;如果“==”两边的是基本数据类型,则比较数值是否相等。

注:八大基本数据类型的封装类型中,除了Float和Double外,其他的都实现了常量池存储,即它们是对象但是“==”比较的是它们的值

关于Integer的坑

Integer a = 127;
Integer b = 128;
System.out.println(a == b); //true

Integer c = 128;
Integer d = 128;
System.out.println(c == d); //false

原因: Integer 在常量池中的存储范围为[-128,127],127在这范围内,因此是直接存储于常量池的,而128不在这范围内,所以会在堆内存中创建一个新的对象来保存这个值,所以m,n分别指向了两个不同的对象地址,故而导致了不相等。

与equals()的区别: equals方法和"= =“完全一样;但是equals方法可以重写;”=="的话,如果是值类型则比较值是否相等;如果是引用类型则判断对象地址是否相等。
关于String的坑

public class Test {
    public static void main(String[] args) {
        String s1 = "HelloWorld";
        String s2 = new String("HelloWorld");
        if (s1 == s2) {
            System.out.println("s1 == s2");
        } else {
            System.out.println("s1 != s2");
        }
        if (s1.equals(s2)) {
            System.out.println("s1 equals s2");
        } else {
            System.out.println("s1 not equals s2");
        }
    }
 }

/*
输出:s1 != s2
	 s1 equals s2
*/

原因:Object 中的equals() 与 “==” 的作用相同,但String类重写了equals()方法,比较的是对象中的内容。


5 若对一个类不重写,它的equals()方法是如何比较的

​ 比较的是对象的地址是否相等,两个对象equals为true则它们严格相等,即内存地址相等


6 说说Lamda表达式的优缺点

优点
1.简洁
2.非常容易并行计算
3.可能代表未来的编程趋势

缺点
1.若不用并行计算,很多时候计算速度没有比传统的for循环快
2.不容易调试
3.若其他程序员没学过lamda表达式,代码不容易让其他语言的程序员看懂


7 一个十进制的数在内存中是怎么存储的

​ 补码的形式


8 为什么有时会出现4.0-3.6=0.40000001这种现象

原因:二进制的小数无法精确的表达十进制的小数,计算机在计算十进制小数的过程中是要先转换成二进制再进行计算的,这过程中出现了误差


9 Java支持的数据类型有哪些,什么是自动拆装箱
数据类型位数字节数
btye81
short162
int324
long648
float324
double648
boolean11
char162

自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间的相互转化。比如:把int转换成Integer,double转换成Double,等等。反之就是拆箱。


10 什么是值传递和引用传递
  • 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量
  • 引用传递一般是对于对象变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,但对引用对象进行操作是会同时改变原对象的

一般认为,Java内的传递都是值传递,以下是一个String和char[]的传递例子,String虽然也是引用类型,但是String被final修饰,其值是不可改变的,所以str传递的是它的副本,副本改变,原str不变

public class Example{
    String str=new String("tarena");
    char[]ch={'a','b','c'};
    public static void main(String args[]){
        Example ex=new Example();
        ex.change(ex.str,ex.ch);
        System.out.print(ex.str+" and "+ex.ch); //tarena and gbc
    }
    public void change(String str,char ch[]){
   		//引用类型变量,传递的是地址,属于引用传递。
        str="test ok";
        ch[0]='g';
    }
}

11 数组(Arrary)和列表(ArrayList)有什么区别,什么时候用Array而不是ArrayList

区别

  • Array可以包含基本类型和对象类型,ArrayList只能包含对象类型
  • Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
  • Array大小是固定的,ArrayList的大小是动态变化的
  • ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。


12 String、StringBuilder、StringBuffer的区别

三者区别

  • StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样
  • String:不可变字符序列,保存在字符常量池里
  • StringBuffer:可变字符序列、效率低、线程安全
  • StringBuilder:可变字符序列、效率高、线程不安全
  • String使用陷阱:
String s = "a"; //创建了一个字符串
s = s + "b"; 
/* 实际上原来的"a"字符串对象已经丢弃了,现在又产生了一个字符串"ab"。
如果多次执行这些改变字符串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。
如果这样的操作放到循环中,会极大影响程序的性能 */
/* ----------------------------------------------------------------------------- */
//初始化上的区别,String可以空赋值,后者不行,报错
String str = null; //可编译通过

/*结果警告:
Null pointer access: The variable result can only be null at this location*/
StringBuffer sb = null; 

//StringBuffer对象是一个空的对象
StringBuffer sb = new StringBuffer();

//创建带有内容的StringBuffer对象,对象的内容就是字符串”
StringBuffer sb = new StringBuffer(“abc”);

小结

1.如果要操作少量的数据用 String;

2.多线程操作字符串缓冲区下操作大量数据 StringBuffer;

3.单线程操作字符串缓冲区下操作大量数据 StringBuilder。


13 我们在web应用开发过程中经常遇到输出某种编码的字符,如iso8859-1等,如何输出一个某种编码的字符
public String translate (String str) {
    String tempStr = "";
    try {
        tempStr = new String(str.getBytes("ISO-8859-1"), "GBK");
        tempStr = tempStr.trim();
    } catch (Exception e) {
        System.err.println(e.getMessage());
    }
    return tempStr;
}

14 &和&&的区别

​ &有两种用法:(1)按位与;(2)逻辑与。而&&是短路与运算。逻辑与和短路与的区别:二者都要求运算符两端的表达式都为true时整个表达式才为true,但是后者之所以称为短路与是因为只有&&的左端表达式为true时,&&的右端表达式才会进行判断,否则直接短路掉不会进行运算,举个例子:用户登录判断用户名不会null且不是空字符串时,应该这样写userName != null && !userName.equals("")二者顺序不能掉换,更不能用&,因为如果第一个条件不成立,则不能进行字符串的equals比较,否则会抛NullPointerExeption异常。
扩展:“||”与“&&“一样,从左边开始只要满足表达式为true,则右边表达式将自动忽略不予运算


关键字


1 介绍一下Synchronize锁,如果用这个关键字修饰一个静态方法,锁住了什么,如果修饰成员方法,锁住了什么

synchronize可以提供原子性、可见性和有序性。

修饰静态方法和同步代码块synchronize(ClassName.Class):锁的是类,线程想要执行对应同步代码需要获得类锁;

修饰成员方法:线程获得的是当前调用该方法的对象实例的对象锁


2 介绍一下volatile

volatile可以提供可见性和有序性,但是无法保证原子性。

可见性:Java内存模型分为主内存和工作内存,线程A从主内存读取变量到自己的工作内存中作+1操作,而此时变量的值没有及时刷新到主内存中,线程B此时在主内存中读到的值很可能是+1前的旧值,而可见性就是volatile修饰的变量在线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

有序性:在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

  2. 它会强制将对缓存的修改操作立即写入主存

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效


3 说一下Synchronize和Lock

synchronize是Java的关键字,可以用来修饰一个方法或者一段代码块,不能修饰变量,能保证在同一时刻最多只能有一个线程执行该段代码,而volatile只能用来修饰变量,不能修饰方法和代码块。

Lock是一个接口,synchronize在发生异常时会自动释放线程占有的锁,所以不会产生死锁现象;而Lock发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁,因此使用Lock时要在finally块中释放锁;Lock可以让等待锁的线程响应中断,而synchronize不行,使用synchronize时,等待的线程会一直等待下去,不能响应中断;通过Lock可以知道有没有获取锁,而synchronize却无法办到。


4 Java中的final都是怎么用的

修饰类:表示这个类不能被继承,该类中的成员变量可以根据需要设为final,即不一定都是final;而该类中的成员方法都会被隐式地指定为final方法。

修饰方法:表示这个方法不能被子类重写,一个类中的private方法会被隐式地指定为final方法。

修饰变量:表示这个变量必须赋初始且值只能被赋值一次,赋值时有两种方式:①在声明时直接赋值②在构造方法中赋值;如果修饰的变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的; 如果是修饰基本数据类型,则其数值一旦被初始化之后就不能被修改也不能重新赋值。


面向对象


1 访问控制修饰符符的作用范围
类内部本包子类外部包
public
protected×
default××
private×××

default和protected的区别是:

前者只要是外部包,就不允许访问。

后者只要是子类就允许访问,即使子类位于外部包。

总结:default拒绝一切包外访问;protected接受包外的子类访问。


2 请列举你知道的Object类的方法
  • Object():默认构造方法
  • clone():创建并返回此对象的一个副本
  • equals():指示某个对象是否与此对象“相等”
  • finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用该方法
  • getClass():返回一个对象的运行时类
  • hashCode():返回该对象的哈希码值,即对象的地址值
  • notify():唤醒在此对象监视器上等待的单个线程
  • notifyAll():唤醒在此对象监视器上等待的所有线程
  • toString(): 返回一个字符串,该字符串由类名(对象是该类的一个实例)、at 标记符“@”和此对象哈希码的无符号十六进制表示组成 ,一般都推荐重写该方法
  • wait():使当前线程进入阻塞状态,直到其他线程调用该对象的notify()或notifyAll(),或者超过指定的时间量

3 重载和重写的区别,相同参数不同返回值能重载吗

重载(Overloading)

1.方法重载是让类以统一的方式处理不同数据类型的一种手段。多个同名方法同时存在,具有不同的参数个数和类型,重载是多态性的一种表现

2.Java的重载就是在一个类中有多个同名方法,但具有不同的参数和不同的定义,调用方法时通过传递给它的不同参数个数和参数类型来决定具体使用哪个方法,这就是多态性

3.重载时,方法名必须一样,但参数类型或参数个数要不一样,返回值可以相同可以不同,无法以返回类型作为重载的区分标准

重写(Overriding)

1.父类与子类之间的多态性即重写,对父类的方法进行重新定义。如果在子类中定义某方法与其父类有相同的名称和参数,则该方法叫做重写。在Java中,子类继承了父类的方法所以不需要重新写相同的方法,但有时子类并不想原封不动地继承父类的方法,而是想做一定的修改,这就是采用了方法的重写,所以方法重写又叫方法覆盖

2.子类重写了父类的方法后,如果需调用父类的原方法,则使用super关键字,该关键字引用了当前类的父类

3.子类重写的方法的访问修饰权限大于等于父类,比如:父类中的方法修饰为protected,则子类重写该方法时只能用protectedpublic修饰


4 简单说一下static关键字,是否可以覆盖一个private或者static的方法

​ “static”关键字表明一个成员变量或一个成员方法可以在没有所属的类的实例变量的情况下被访问。static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的,static方法类的任何实例都不相关,所以概念上不适用。

​ Java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。


5 类加载机制,双亲委派模型的好处

原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

好处

  • 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
  • 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

6 讲讲什么是泛型

​ 泛型,即“参数化类型”,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以成为类型形参),然后在调用/使用时传入具体的类型。

​ 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。


7 如何通过反射创建对象

方法1:通过类对象调用newInstance()方法,此时该类对象必须要有无参数的构造方法,否则无法创建,如:

Class<String> c = String.class;
//直接newInstance的话必须保证User的默认的构造方法(即无参)正常存在,也就是没有被私有化!这是前提条件
String str = c.newInstance();

//上面两行代码也可以直接写成这样
String str = String.class.newInstance();

方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,如:

String str = String.class.getConstructor(String.class).newInstance("Hello")

8 接口和抽象类的区别
  • 类只可以继承一个抽象类,但能实现多个接口
  • 类可以不实现抽象类和接口中声明的所有方法,但此时该类也必须声明成是抽象的
  • 接口中的所有方法都是抽象的,而抽象类中可以有非抽象方法
  • 接口中的变量必须用public static final修饰,并且需要给出初始值,抽象类可以包含非final的变量
  • 接口中的成员方法默认是public abstract且只能是这个类型,不能是static,也不允许子类覆写,抽象类中允许有static 的方法

9 Comparable接口和Comparator接口是干什么的,说说它们的区别

Comparable:若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序,该接口只有一个compareTo方法,定义如下

public interface Comparable<T> 
{
    public int compareTo(T o);
}

compareTo方法:传入一个指定对象, 比较该类对象与指定对象的顺序,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数

Comparator: 我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),我们可以通过实现Comparator接口来新建一个比较器,然后通过这个比较器对类进行排序,它包含两个方法:equals()和compare()

public interface Comparator<T>
{
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

注意

1.若一个类要实现Comparator接口:它一定要实现compare(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数

2.int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”

区别

  • Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序
  • Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”
  • 两种方法各有优劣,用Comparable 简单,只要实现Comparable接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。用Comparator 的好处是不需要修改源代码,而是另外实现一个比较器,当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了,并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了

10 final、finally、finalize的区别
  • final用于声明属性、方法和类,分别表示属性不可变,方法不可覆盖、类不可继承
  • finally是异常处理语句结构的一部分,表示总是执行
  • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的该方法,可以覆盖此方法提供垃圾收集时的其他资源回收,如关闭文件等

11 静态内部类(Static Nested Class)和内部类(Inner Class)的不同
  • 创建一个static内部类的对象,不需要一个外部类对象
  • 不能从一个static内部类的一个对象访问一个外部类对象

静态内部类示例

类定义:

public class OutClass1 {   
    public int oid;   
    public String oname;   
    public static class InnerStaticClass1{   
        public int iid;   
        public String iname;   
    }   
}

实例化:

OutClass1 oc = new OutClass1();//外部类实例化
InnerStaticClass1 ic = new InnerStaticClass1();//静态内部类实例化

内部类示例

类定义:

public class OutClass2 {   
    public int oid;   
    public String oname;   
    public class InnerClass2   
    {   
        public int iid;   
        public String iname;   
    }   
}

实例化:

OutClass2 oc=new OutClass2();//先实例化外部类
OutClass2.InnerClass2 ic=oc.new InnerClass2();//才能实例化内部类

12 内部类可以引用其外部类的成员吗
  • 内部类如果不是static,则它可以访问它的外部类对象的所有属性和方法
  • 内部类如果是static,则它只可以访问它的外部类对象的所有static属性和方法

13 两个对象值相同(x.equals(y) == true),那它们的hashcode可以不相同

​ 错误。如果x.equals(y) == true,那么它们的hashcode应当相同,Java中对于equals和hashcode是这样规定的:

  • 如果两个对象相等(equals返回true),那么它们的hashcode一定相同
  • 如果两个对象的hashcode相同,它们不一定相等

14 如何通过反射获取和设置对象私有字段的值

​ 可以通过类对象的getDeclaredField()方法获取字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了

//得到对象的所有私有属性
Field field = obj.getClass().getDeclaredField();
//通过反射给指定字段赋值
field.set(obj, "Hello");

15 谈谈Java的六原则一法则

六原则

1.单一职责原则:一类只做它该做的事

2.里氏替换原则:子类必须能够替换基类(父类),否则不应当设计为其子类

3.依赖倒换原则:设计要依赖于抽象而不是具体化

4.接口隔离原则:接口要小而专,不能大而全

5.开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

6.组合/聚合复用原则:尽量使用组合和聚合,少使用继承的关系来达到复用的原则

一法则

迪米特法则:低耦合,高内聚


16 Query接口的list方法和iterator方法有什么区别
  • list()方法无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存;iterate()方法可以充分利用缓存,如果目标数据只读或者读取频繁,使用iterate()方法可以减少性能开销
  • 查询策略不同。获取数据的方式不一样,list会直接查询数据库,iterate会先到数据库中把id取出来,然后真正要遍历某个对象的时候先到缓存中找,如果找不到,以id为条件再发一条sql到数据库,这样如果缓存中没有数据,则查询数据库的次数为n+1,即list不会造成N+1问题,iterator可能造成N+1问题

集合部分


1 HashMap和CurrentHashMap的区别
  • HashMap是线程不安全的,在多线程情况下put时,会形成环导致死锁
  • CurrentHashMap是线程安全的,采用分段锁机制,减少锁的粒度

2 简单描述一下HashMap的实现原理

1.利用key的hashcode重新hash计算出当前对象的元素在数组中的下标

2.存储时,如果key是null,则把这对key-value存入数组的第0项中;如果出现hash值相同的key,分两种情况:

  • 如果key相同,则新的value值覆盖掉旧的value值
  • 如果key不同,即发生哈希冲突,则将当前的key-value放入链表中(在JDK1.8之后,如果链表长度大于等于8,则将链表转换成红黑树,如果链表长度小于等于6,则将树转换成链表),这种解决哈希冲突的方法叫拉链法

3.获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值


3 如果HashMap的key是一个自定义的类怎么办

​ 使用HashMap时如果key是自定义的类,则必须重写hashCode()和equals(),因为key的比较就是要用到这两个方法,不重写的话会导致两个相同值的对象因为hashcode(即地址值)不一样而被存两次。


4 ArrayList和LinkedList的区别
  • ArrayList采用数组实现,查找效率比LinkedList高
  • LinkedList采用双向链表实现,插入和删除的效率比ArrayList高

如果一直在list的尾部添加元素,则LinkedList的效率要高


5 HashMap底层,负载因子,为什么是2^n

​ 一个哈希表,为了减少哈希碰撞,我们首先想到的方法是取余运算,因为取余就可以使得整数均匀地分布到哈希表的每一个位置上。比如现在的哈希表的长度是16,那么11如果要放到哈希表上,那就用11%16,得到了11,那么11就放在索引为11的位置。但是呢,计算机的取余操作效率不高,而位运算的效率是很高的,所以为了提高效率,java的工程师在原码里面就用了x&(length-1)的操作,这里的长度16-1 = 15,11和15做位运算,就是11&(1111),最后得到的结果也是11,也就是说,x&(length-1)的结果是和x%length的结果是一样的,因为当length为2的整数次幂时,length-1的二进制位数上都为1,这样就保证了取余和位运算的结果相等,所以原码为了提高效率就要求长度为2的整数次幂。


6 TreeMap底层,红黑树原理

​ TreeMap的实现就是红黑树数据结构,它是一颗自平衡的排序二叉树,可以保证有需要时快速检索指定节点。

​ 红黑树的插入、删除、遍历时间复杂度都是O(lgN),所以性能上低于哈希表,但是哈希表无法提供键值对的有序输出,红黑树是排序输入的,所以可以按照键的值的大小有序输出,红黑树性质如下:

1.每个节点要么是红色,要么是黑色

2.根节点永远是黑色的

3.所有的叶节点都是空节点(即NULL),并且是黑色的

4.每个红色节点的两个子结点都是黑色(即从每个叶子到根的路径上不会有两个连续的红色节点)

5.从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点


7 Java集合类框架的基本接口有哪些
  • Collection:代表一组对象,每一个对象都是它的子元素
  • Set:不包含重复元素的Collection
  • List:有顺序的Collection,并且可以包含重复元素
  • Map:可以把键(Key)映射到值(Value)的对象,键不能重复,否则会覆盖掉原来的值

8 为什么集合类没有实现Cloneable和Serializeable接口

​ 克隆(cloning)或者序列化(serialization)的语义和含义是跟具体的实现相关的。因此应该由集合类的具体实现类来决定如何被克隆或者序列化。

什么是克隆?

​ 克隆是把一个对象里面的属性值,复制给另一个对象。而不是对象引用的复制。

实现Serializable序列化的作用

1.将对象的状态保存在存储媒体中以便可以在以后重写创建出完全相同的副本

2.按值将对象从一个应用程序域法相另一个应用程序域

​ 实现Serializable接口的作用就是可以把对象存到字节流,然后可以恢复。所以你想你的对象没有序列化,怎么才能在网络传输呢?要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化。如果你不需要分布式应用,那就没必要实现序列化。


9 什么是迭代器

​ Iterator提供了统一遍历操作集合的统一接口,Collection接口实现了Iterable接口,每个集合都通过实现Iterable接口中的iterator()方法返回Iterable接口的实例,然后对集合的元素进行迭代操作。

注意:在迭代元素的时候不能通过集合的方法删除元素,否则会抛出ConcurrentModificationException异常,但是可以通过Iterable接口中的remove()方法进行删除。


10 Iterator和ListIterator的区别

区别

  • Iterator可以用来遍历Set和List集合,但是ListIterator只能用来遍历List
  • Iterator对集合只能前向遍历,而ListIterator既可以前向也可以后向
  • ListIterator实现了Iterator接口,并包含其他的功能,如增加元素、替换元素、获取前一个和后一个的索引等

11 快速失败(fail-fast)和安全失败(fail-safe)的区别

快速失败(fail-fast):在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出 ConcurrentModification Exception。

  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果结构发生变化,就会改变modCount 的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历
  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug
  • 场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)

安全失败(fail-safe):采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModification Exception
  • 缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的
  • 场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改
fail-fastfail-safe
是否抛ConcurrentModificationException异常
是否克隆集合
是否有内存开销
常见例子HashMap,Vector,ArrayList,HashSetCopyOnWriteArrayList

12 Collection和Collections的区别
  • Collection是集合类的上级接口,继承于它的接口主要是List和Set
  • Collections是针对集合类的一个帮助类,它提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

二、Java高级知识

线程


1 多线程中的i++线程安全吗,为什么

​ 不安全,i++不是原子性操作,i++的具体步骤是这样的:先读取i的值,然后对i值加一,最后再重新赋值给i,执行期间任何一步都是有可能被其他线程抢占的。


2 如何线程安全的实现一个计数器

​ 可以使用加锁,比如synchronize或者lock,也可以使用Concurrent报下的原子类


3 介绍一下生产者消费者模式

生产者消费者是线程模型中的经典问题:生产者和消费者在同一时间内共用统一存储空间,生产者向空间里生产数据,而消费者取走数据。

生产者 => 缓冲区 => 消费者

优点:支持并发、解耦


4 创建线程的方法,哪个更好
  • 继承Thread类并重写run方法
  • 实现Runnable接口并重写run方法
  • 使用Callable和Future创建线程
  • 使用线程池例如用Executor框架

实现Runnalbe接口更好,实现Runnable接口创建的线程可以处理同一资源,从而实现资源的共享


5 Java中的几种常见线程池

1.newFixedThreadPool:创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,如果工作线程量达到线程池初始的最大数,则将提交的任务存入到池队列中

2.newCacheThreadPool:创建一个可缓存的线程池,其特点是:

  • 工作线程的数量几乎没有限制(上线是Integer.MAX_VALUE),这样可以灵活地往线程池中添加线程
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止,终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程

3.newSingleThreadExecutor:创建一个单线程化的Executor,即只创建唯一的工作线程来执行任务,如果这个线程异常结束,会有另一个线程取代它,保证顺序执行。特点是保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的

4.newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行


6 线程池的好处
  • 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能执行
  • 提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以统一分配、调优和监控

7 同步方法和同步代码块的区别
  • 同步方法默认用this或者当前类class对象作为锁
  • 同步代码块可以选择以什么来加锁,比同步方法更细颗粒度,我们可以选择只同步需要同步的部分代码块而不是整个方法

8 在监视器内部,是如何做线程同步的

​ 监视器和锁在Java虚拟机中是一块使用的,监视器见识一块同步代码块,确保一次只有一个线程执行同步代码块,每一个监视器都和一个对象引用相关联,线程在获得锁之前不允许执行同步代码块。


9 线程的生命周期,五个状态

1.新建(NEW):新创建一个线程对象

2.可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权

3.运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码

4.阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态,造成阻塞状态的三种情况:

  • 等待阻塞:运行(running)的线程执行wait()方法,JVM会把该线程放入等待队列(waitting queue)中
  • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中
  • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态

5.死亡(DEAD):线程run()、main() 方法执行结束,或因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生,在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常


10 sleep()和wait()的区别
  • sleep()是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep不会释放对象锁
  • wait()是Object类的方法,导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify或notifyAll方法后本线程才会进入对象锁定池准备获得对象锁进入运行状态

11 sleep()和yield()的区别
  • sleep方法给其他线程运行机会时不考虑线程的优先级,而yield方法只会给相同或更高优先级的线程以运行的机会
  • 线程执行sleep方法后进入阻塞状态,而执行了yield方法后进入就绪状态
  • sleep方法需要声明抛出InterruptedException异常,而yield方法没有声明任何异常
  • sleep方法比yield方法具有更好的可移植性

12 当一个线程进入一个对象的synchronize方法A之后,其他线程能否进入该对象的synchronize方法B

​ 不能,其他线程只能访问该对象的非同步方法,同步方法则不能进入,因为非静态方法上的synchronize修饰符要求执行方法时获得对象的锁,如果已经进入了方法A,证明对象锁已经被取走,那么试图进入方法B的线程只能在等锁池中等待对象的锁。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值