首页 > Java常见面试题
头像
youngwong
编辑于 2021-07-13 22:39
+ 关注

Java常见面试题

持续更新,此前发布过的帖子:

数据库常见面试题:https://www.nowcoder.com/discuss/662289?source_id=profile_create_nctrack&channel=-1

操作系统常见面试题:https://www.nowcoder.com/discuss/663233?source_id=profile_create_nctrack&channel=-1

大纲:

1.Object类中有什么方法,有什么作用?
2.Collection接口有什么方法? 结构如何?
3.hashMap如何添加元素?如何判断相等?底层如何实现?
4.hashMap、ArrayList线程不安全性如何解决?
5.List和Set的区别
6.List常用的实现类
7.ArrayList和LinkedList的区别
8.Set常用的实现类
9.HashSet和LinkedHashSet的区别
10.int和Integer的区别
11.==和equals的区别
12.final关键字
13.接口和抽象类的区别
14.Java的异常处理机制,Error和Exception的区别
15.Java多态,重写和重载
16.LinkedList为什么增删快、查询慢



1.Object类中有什么方法,有什么作用?

1.clone方法

保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。

主要是JAVA里除了8种基本类型传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里讲参数改变,这是就需要在类中复写clone方法。

2.getClass方法

final方法,获得运行时类型。

3.toString方法

该方法用得比较多,一般子类都有覆盖。

4.finalize方法

该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。

5.equals方法

该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。

6.hashCode方法

该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。

一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。

如果不重写hashcode(),在HashSet中添加两个equals的对象,会将两个对象都加入进去。

7.wait方法

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。

调用该方法后当前线程进入睡眠状态,直到以下事件发生。

(1)其他线程调用了该对象的notify方法。

(2)其他线程调用了该对象的notifyAll方法。

(3)其他线程调用了interrupt中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。

8.notify方法

该方法唤醒在该对象上等待的某个线程。

9.notifyAll方法

该方法唤醒在该对象上等待的所有线程。

2.Collection接口有什么方法? 结构如何?

boolean add(Object o) 向集合中添加元素

int size() 获取集合中元素的个数

void clear() 清空集合

boolean contains(Object o) 判断集合中是否有o

boolean remove( Object o) 删除某个元素

boolean isEmpty() 判断集合是否为空

Object[] toArray 转换成数组的方法

结构图如下:

Collection是集合类的根接口,其实现Iterable迭代器接口,所以其所有实现类都可通过迭代器遍历集合内元素。Collection下有Set,Queue,List 三个子接口。
每个接口的特性如下:

List集合为元素存取有序的集合,该接口的实现类可以精确的操作集合的每个元素,集合可以包含重复元素;

Set集合不允许包含相同的元素,而判断两个对象是否相同则是根据equals方法。

Queue用于模拟队列这种数据结构,遵循先进先出原则。通常只是对集合的首尾进行操作,并不对集合内部进行操作

3.hashMap如何添加元素?如何判断相等?底层如何实现?

HashMap:底层是基于数组+链表 + 红黑树,非线程安全的,默认容量是16、允许有空的健和值。
初始size为16,扩容:newsize = oldsize << 1,size一定为2的n次幂。
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀计算index方法:index = hash & (tab.length – 1)。
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。

HashMap的存取过程,当执行putVal的操作的时候,

1.首先检查大小,看是否需要扩容(默认元素超过最大值的0.75时扩容),如果需要扩容就进行扩容

2.然后计算出key的hashcode,根据hashcode定位数值所在的bucketIndex

3.如果该位置上没有元素,就直接插入,结束

4.如果该位置上有元素就使用equal比较是否相同

5.如果key相同就把新的value替换旧的value,结束

6.如果key不同,就继续遍历,找到根节点,如果没找到key的话,就构造一个新的节点,然后把节点插入到链表尾部,表示put成功(jdk 1.8 之后链表长度超过8就会转化为红黑树)

4.hashMap、ArrayList线程不安全性如何解决?

故障现象 :ArrayList的add()方法并没有使用synchronized所以是线程不安全的,会造成java.util.ConcurrentmodificationException(并发修改异常)

导致原因:并发争抢修改导致

可以通过以下三个方法解决ArrayList 线程不安全的问题:

1、使用Vector集合,该集合同样实现了List接口,但是它的add()方法加上了synchronized保证了线程安全性;

2、使用集合工具类Collections,该类中封装了很多关于集合操作的方法,方法synchronizedList(new ArrayList<>())便是对ArrayList进行了封装,使其解决不安全问题;

3、使用写时复制CopyOnWriteArrayList进行操作:

往容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行复制出一个新的object[] newElements‘

然后向新容器object[] newElements里面添加元素;

添加元素后再将原容器的引用指向新的容器setArray(newElements)。

这样的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁。因为当前容器不会添加任何容器。所以copyOnwrite容器也是一种读写分离的思想,读和写不同的容器。

HashSet和HashMap 的解决办法同ArrayList类似。其在多线程操作时也会报并发修改异常的错误。其中Collections中有synchronizedMap()和synchronizedSet()方法。还可以使用CopyOnWriteArraySet<>()和ConcurrentHashMap<>()方法。

5.List和Set的区别

List和set都是继承collection接口,存放对象,不同的是list存放有序可以重复的元素,set存放的无序不可重复元素,list比较常用的实现类有ArrayList和LinkList,Set是HashSet和TreeSet。

set如何实现不可重复元素的

先判断hashcode是否相等,如果hash码值不相同,说明是一个新元素,存,不等在通过equals判断,如果qules判断相等,说明元素已经存在,不存

ArrayList和LinkedList的区别

ArrayList内部使用数组的形式实现了存储,在内存中,数组是一块连续的区域,所以在需要频繁读取集合中的元素时,使用ArrayList效率较高,LinkList是用链表的形式实现,插入和删除操作较多时,使用LinkedList效率较高。

HashSet和TreeSet的区别

HashSet是通过哈希表实现的,存的是无序数据,TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值。

6.List常用的实现类

ArrayList:

底层数据结构是动态数组,查询快,增删慢。当调用无参构造方法创建ArrayList对象时,默认大小为10。当后期数组需要扩容时,会产生一个新的数组,该数组大小为原来数组大小的1.5倍,然后再把原数组元素复制到新数组中。

常用方法:get(int index)、set(int index, E e)、add(E e)、remove(E e)、remove(int index)等。

LinkedList:

底层数据结构基于双向链表,增删快,顺序方法效率高,随机访问效率低,线程不安全。
LinkedList实现了List接口,也继承了get(int i)等含有索引的方法。而LinkedList本身是做为链表的,那么它的索引又是从何而来呢?
当我们传入一个索引值时,首先会将索引值与当前链表长度做右移一位运算后进行比较,如果index更小,就将从头到尾进行遍历;反之则从尾到头遍历直到目标位置。目的是为了减少遍历的次数。

Vector:

Vector即是线程安全的ArrayList,但与之对应的其效率则有所下降。与ArrayList相比,Vector的扩容机制有所不同。每一次默认构建的新数组大小为原来的2倍。

7.ArrayList和LinkedList的区别

(1) ArrayList是一个泛型类,底层采用可调整大小的数组实现,通过索引可以对元素进行快速随机访问,所以时间复杂度是O(1)的。但在向数组中间进行插入,删除操作时,由于要进行索引移动,所以增删慢,其时间复杂度是O(n)的;LinkedList是一个泛型类,底层是一个双向链表,其在头部的添加,删除操作只需改变指针指向即可,其时间复杂度是O(1)的。在中间的删除以及添加也需要先进行查找,所以时间复杂度是O(n)的。当进行查询操作时,需要移动指针从前往后依次查找,所以所以时间复杂度是O(n)的。

(2) ArrayList实现RandomAccess接口,而LinkedList没实现。RandomAccess接口是一个标记接口,用以标记实现的List集合具备快速随机访问的能力。由于ArrayList基于数组实现,天然带下标,可以实现常量级的随机访问,复杂度为O(1),而LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)。所以ArrayList具备快速随机访问功能,而LinkedList没有。当一个List拥有快速访问功能时,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速。当我们不明确获取到的是Arraylist,还是LinkedList的时候,我们可以通过RandomAccess来判断其是否支持快速随机访问,若支持则采用for循环遍历,否则采用迭代器遍历。可以采用 instanceof 关键字来判断类是否具备快速随机访问功能。

(3) LinkedList实现deque,queue接口,所以支持队列操作,堆栈操作;

(4) 由于ArrayList,LinkedList的所有方法都是默认在单一线程下进行的,因此LinkedList,ArrayList不具有线程安全性。若想在多线程下使用,应该使用Colletions类中的静态方法synchronizedList()对ArrayList/LinkedList进行调用即可。


8.Set常用的实现类

一.HashSet

HashSet底层数据结构采用的是哈希表,元素无序(即不保证元素的排列顺序不会发生改变)且不可重复,增删操作的时间复杂度都是O(1),元素可以且只可以存在一个null值。

另外,HashSet集合判断两个元素是否相等的标准是equals()方法返回值为true且hashCode()方法返回值也相等。

事实上,当一个元素存入HashSet集合中的时候会调用hashCode方法获得其hashCode值,然后根据hashCode值来决定元素在内存中的存储位置。

二.LinkedHashSet

LinkHashSet底层利用哈希表唯一确定元素的存储位置以保证唯一性,同时依靠链表实现元素的插入有序排列(即使得元素的添加顺序将会与元素的访问次序一致)。

三.TreeSet

TreeSet集合底层结构是红黑树(平衡二叉查找树),用于实现对元素的排序(可自定义排序规则),不允许元素重复且不能存在null值

注意:如果当TreeSet元素对象是引用数据类型时,由于不知道该对元素采用何种排序方式,编译器将会报错。这个时候就应该采用自定义排序,实现Comparator接口并重写compare()方法

例子:

//自定义App类的比较器:
public class AppComparator implements Comparator<App> {

    //比较方法:先比较年龄,年龄若相同在比较名字长度;
    public int compare(App app1, App app2) {
        int num = app1.getAge() - app2.getAge();
        return num == 0 ? app1.getName().length() - app2.getName().length() : num;
    }}


public class App{

    private String name;

    private Integer age;

    ………………………………
public class TreeSetTest {
    public static void main(String[] agrs){
        customSort();
    }

    //自定义比较器:升序
    public static void customComparatorSort(){
        TreeSet<App> treeSet = new TreeSet<App>(new AppComparator());

        //排序对象:
        App app1 = new App("hello",10);
        App app2 = new App("world",20);
        App app3 = new App("my",15);
        App app4 = new App("name",25);

        //添加到集合:
        treeSet.add(app1);
        treeSet.add(app2);
        treeSet.add(app3);
        treeSet.add(app4);

        System.out.println("TreeSet集合顺序为:"+treeSet);
    }}

9.HashSet和LinkedHashSet的区别

HashSet

1.不能保证元素的排列顺序,顺序有可能发生变化

2. 不是同步的

3. 集合元素可以是null,但只能放入一个null

当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中存储位置。

简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等

注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,

其   hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

LinkedHashSet

LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。

LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

10.int和Integer的区别

1、Integer是int的包装类,int则是java的一种基本数据类型

2、Integer变量必须实例化后才能使用,而int变量不需要

3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。

4、Integer的默认值是null,int的默认值是0

11.==和equals的区别

(1)、基础类型比较

使用==比较值是否相等。

(2)、引用类型比较

①重写了equals方法,比如String。

第一种情况:使用==比较的是String的引用是否指向了同一块内存

第二种情况:使用equals比较的是String的引用的对象内用是否相等。

②没有重写equals方法,比如User等自定义类

==和equals比较的都是引用是否指向了同一块内存。

下图在这里多了一个intern方法。他的意思是检查字符串池里是否存在,如果存在了那就直接返回为true。因此在这里首先s1会在字符串池里面有一个,然后s2.intern()一看池子里有了,就不再新建了,直接把s2指向它。

12.final关键字

1、用来修饰一个引用

1.  如果引用为基本数据类型,则该引用为常量,该值无法修改;

2.  如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。

//2.地址不能修改,但是对象本身的属性可以修改

final int[] arr = {1,2,3,45};

arr[3] = 999;

//arr = new int[]{1,4,56,78};

3.  如果引用时类的成员变量,则必须当场赋值,否则编译会报错。

2.用来修饰一个方法

当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。

3.用来修饰类

当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。

13.接口和抽象类的区别

(一) 继承方面:

抽象类只能单继承;而接口可以多实现;
(二) 成员属性方面:

抽象类中可以有普通属性,也可以有常量;
接口中的成员变量全部默认是常量,使用public static final修饰,这个可以省略不写;
(三) 代码块方面:

抽象类可以含初始化块;接口不能含初始化块;
(四) 构造函数方面:

抽象类可以有构函数,但是这里的构造函数不是用来创建对象的,而且用来被实现类调用进行初始化操作的;
接口不能有构造函数;
(五) 方法方面:

接口在JDK1.8之后可以定义抽象方法(无方法体)、default 修饰的默认方法(有方法体)、static修饰的静态方法(有方法体),JDK1.8以前是只能有抽象方法。
抽象类中除了静态方法和抽象方法外还可以有普通方法。

相同之处:

接口与抽象类都不能被实例化,需要被其他进行实现或继承。
接口与抽象类里面都能包含抽象方法,实现接口或继承抽象类的子类都必须实现这些抽象方法。

14.Java的异常处理机制,Error和Exception的区别

https://blog.csdn.net/hl_java/article/details/76837141

Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的错误条件。当条件生成时,错误将引发异常。

Java所有异常类都是 Throwable的子类。它包括Java异常处理的两个重要子类:Error和Exception.

① Exception 和Error 都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常处理机制的基本类型。

② Exception和Error体现了Java平台设计者对不同异常情况的分类。

⑴Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

⑵Exception又分为可检查(checked)异常和不可检查(unchecked)异常。可检查异常在源代码里必须显式的进行捕获处理,这是编译期检查的一部分。不可检查时异常是指运行时异常,像NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

⑶Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常的、不可恢复的状态。既然是非正常情况,不便于也不需要捕获。常见的比如OutOfMemoryError之类都是Error的子类。

15.Java多态,重写和重载

重写(Override)

重写指的是子类对父类可允许访问的方法进行重新编写,两者有相同的名称,相同参数,相同返回值,但是内容却不相同,子类的新方法将覆盖父类中原有的方法。

重载(Overload)

重载是在同一个类中,方法名称相同,参数不同。这样同名不同参的方法被称为重载。

重载VS重写

重写规则:

1、父类成员方法只能被它的子类重写

2、子类方法的访问修饰符一定要大于父类的访问修饰符(public>protected>default>private)。

3、子类和父类在同一包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。

4、子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。

5、声明为final的方法不能被重写。

6、声明为static的方法不能被重写,但是能够被再次声明。

7、子类方法的参数列表、方法名称、返回值必须和父类方法一致

8、构造方法不能被重写。

9、如果不能继承一个方法,则不能重写这个方法。

重载规则:

1、被重载的方法必须改变参数列表(参数个数或类型不一样);

2、被重载的方法可以改变返回类型;

3、被重载的方法可以改变访问修饰符;

4、被重载的方法可以声明新的或更广的检查异常;

5、方法能够在同一个类中或者在一个子类中被重载。

6、无法以返回值类型作为重载函数的区分标准。

多态

多态的概念,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。

实现多态,需要三个条件:

1、即有类继承或者接口实现。

2、子类要重写父类的方法

3、父类的引用指向子类的对象。

16.LinkedList为什么增删快、查询慢

· ArrayList:长于随机访问元素,中间插入和移除元素比较慢,在插入时,必须创建空间并将它的所有引用向前移动,这会随着ArrayList的尺寸增加而产生高昂的代价,底层由数组支持。

· LinkedList:通过代价较低的在List中间进行插入和删除操作,只需要链接新的元素,而不必修改列表中剩余的元素,无论列表尺寸如何变化,其代价大致相同,提供了优化的顺序访问,随机访问相对较慢,特性较ArrayList更大,而且还添加了可以使其作为栈、队列或双端队列的方法,底层由双向链表实现。

ArrayList内部是数组结构,LinkedList内部是链表结构,ArrayList增删慢,因为涉及到内部数组的扩容和移位,而查询非常快,因为查询时会将索引转换为数组索引,LinkedList增删块,因为只需赋值几次的引用即可,但查询慢,总要从链表的一端开始逐个检索。

LinkedList的查询逻辑

· 根据传入的index去判断是否为LinkedList中的元素,判断逻辑为index是否在0和size之间,如果在则调用node(index)方法,否则抛出IndexOutOfBoundsException;

· 调用node(index)方法,将size右移1位,即size/2,判断传入的size在LinkedList的前半部分还是后半部分

· 如果在前半部分,即index < size/2,则从fisrt节点开始遍历匹配

· 如果在后半部分,即index > size/2,则从last节点开始遍历匹配



全部评论

(0) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期精华帖

热门推荐