java面试题(一)java基础知识

本题目的大纲来自牛客网的资源分享,我个人对题目进以充实,搜集答案。

如有错漏,请及时指正,共同学习、进步。

仅为校招面试准备,本文无详细源码,请见谅。

大纲链接:https://www.nowcoder.com/discuss/365165?type=2

 

目录

1.String、StringBuilder、StringBuffer的区别

2.Java核心数据结构(List、Map、Set)

2.1 List

ArrayList、LinkedList、vector、CopyOnWriteArrayList的区别

CopyOnWriteArrayList的原理

2.2Map

Map的实现

hashmap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap的区别及实现原理

Hashmap的原理、底层、扩容机制

Hashtable的原理、底层、扩容机制

ConcurrentHashMap的原理、底层(重点)

2.3 Set

Set的实现

HashSet、LinkedHashSet、TreeHashSet的区别及原理

3.零散的知识点

封装、继承、多态(面向对象编程的三大特性)

重载和重写的区别

final、finally、finalize

Static关键字

i++与++i

equals()与"=="的区别?

重写equals()方法为什么要同时重写hashcode()方法

this和super的区别

自动装箱与自动拆箱

异常

Java中throw和throws的区别

 


1.String、StringBuilder、StringBuffer的区别  

(1)运行速度     StringBuilder > StringBuffer > String。

         原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改        的,但后两者的对象是变量,是可以更改的。

(2)线程安全   StringBuilder是线程不安全的,而StringBuffer是线程安全的。

(3)使用场景

          String:适用于少量的字符串操作的情况。

     StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。

     StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况。

2.Java核心数据结构(List、Map、Set)

2.1 List

  • ArrayList、LinkedList、vector、CopyOnWriteArrayList的区别

              (1)线程安全性

                       线程安全:Vector,CopyOnWriteArrayList,Collections.synchronizedList()

                       线程非安全:ArrayList,LinkedList

              (2)底层存储结构

                       数组:ArrayList, Vector, CopyOnWriteArrayList

                       链表:LinkedList

              (3)  使用场景

ArrayList:底层为数组,适合随机访问;删除不会引起数组容量变小;动态插入可能涉及到数组长度重新分配;为避免频繁的数组扩容,可设置一个合适的初始容量;不适用于频繁的在数组中间进行插入删除的场景。

LinkedList:底层为双向链表,适合频繁删除新增的场景;随机访问不友好,需要遍历。

Vector:线程安全;所有的方法都加锁,导致性能较差。

CopyOnWriteArrayList:线程安全;修改时,会拷贝一份内容出来,对拷贝的结果进行操作,最后覆盖之前的内容;每次修改都会先上锁,然后进行数组拷贝,所以性能较ArrayList低;读取无锁,所以读的性能比Vector高(没有竞争);默认初始容量为0;遍历和读取都是基于访问时刻列表中的数组进行的;在执行过程中,链表发生修改不会影响 遍历和读取的结果(即此时访问的依然是原数组内容)。

               (4)  特性

1)List是有序的

2) ArrayList和vector默认容量都是为10,扩容规则:增加原来空间大小的一半(1.5),如果依然塞不下,则扩充到正好填充满的情况。而vector增加到2倍

3)LinkedList,CopyOnWriteArrayList默认容量为0。

 4.)new ArrayList<>()内部的数组实际上引用的是一个空数组。

5) 需要线程安全的场景,使用CopyOnWriteArrayList(并发读性能好)或Collections.synchronizedList(并发写  性能好)来替代Vector。

 6) Collections.sort(list, new Comparator(){xxx});

若List中的元素,实现了Comparable接口后,可以直接调用Collections.sort(list)。

                  

  • CopyOnWriteArrayList的原理

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则 通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中 类似的容器还有CopyOnWriteSet。写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,使用迭代器进行遍历时候,也就不会抛出  ConcurrentModificationException异常。

缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁 GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

2.2Map

  • Map的实现

               Map 底层:数组,数组的每一项都是一个链表。

                                             

                                  

            

  • hashmap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap的区别

               

            

  • Hashmap的原理、底层、扩容机制

     HashMap的常见参数

initialCapacity    默认初始容量   值为16,最大容量值为2^30
loadFactor         默认加载因子   值为0.75f
threshold          阈值           默认值为16 *0.75 ,即容量*加载因子

在jdk1.7中,hashmap的底层创建的是Entry[]数组,在实例化后,底层就创建了一个长度为16的Entry[]数组,此时的底层结构是数组+链表;

在jdk1.8中,底层创建的是Node[]数组,底层在一开始并不会创建数组,在第一次调用put方法时,底层才会创建一个长度为16的Node[]数组,此时的底层结构是数组+链表+红黑树。 

扩容:

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素,HashMap并不会去初始化或者扩容table。当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化。

resize发生在table初始化, 或者table中的节点数超过threshold值的时候, threshold的值一般为负载因子乘以容量大小.

每次扩容都会新建一个table, 新建的table的大小为原大小的2倍.

扩容时,会将原table中的节点re-hash到新的table中, 但节点在新旧table中的位置存在一定联系: 要么下标相同, 要么相差一个oldCap(原table的大小).

  • Hashtable的原理、底层、扩容机制

原理与hashmap基本一致。

Hashtable扩容的数组长度旧数组长度乘以2加1:Hashtable中数组的长度尽量为素数或者奇数,同时Hashtable采用取模的方式来计算数组下标,这样减少Hash碰撞,计算出来的数组下标更加均匀。但是这样效率会比HashMap利用位运算计算数组下标低。

Hashtable采用头插法的方式迁移数组:采用头插法的方式效率更高。如果采用尾插法需要遍历数组将元素放置到链表的末尾,而采用头插法将结点放置到链表的头部,减少了遍历数组的时间,效率更高。

JDK1.8前HashMap也是采用头插法迁移数据,多线程情况下会造成死循环,JDK1.8对HashMap做出了优化,JDK1.8Hashtable采用头插法的方式迁移数据:Hashtable是线程安全的,所以Hashtable不需要考虑并发冲突问题,可以采用效率更高的头插法。

Hashtable使用synchronized来实现线程安全,在多并发的情况下效率低下,渐渐被弃用。

  • ConcurrentHashMap的原理、底层(重点)

ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率。多线程安全,key/value不为空。

基本设计理念就是切分成多个Segment块,默认是16个,也就是说并发度是16,可以初始化时显式指定,后期不能修改,每个Segment里面可以近似看成一个HashMap,每个Segment块都有自己独立的ReentrantLock锁,所以并发操作时每个Segment互不影响。这也正是ConcurrentHashMap使用的分段锁技术。当线程占用其中一个Segment时,其他线程可正常访问其他段数据。

jdk1.7升级到1.8,ConcurrentHashMap的变化:
锁方面:由分段锁(Segment继承自ReentrantLock)升级为CAS+synchronized实现;
数据结构层面:将Segment变为了Node,每个Node独立,原来默认的并发度16,变成了每个Node都独立,提高了并发度;
hash冲突:1.7中发生hash冲突采用链表存储,1.8中先使用链表存储,后面满足条件后会转换为红黑树来优化查询;
查询复杂度:1.7中链表查询复杂度为O(N),1.8中红黑树优化为O(logN))

链表长度超过8要转为红黑树:
默认的是链表结构,并不是一开始就是红黑树结构,因为链表比红黑数占用的空间较少;
hash冲突导致链表长度真正达到8的概率极小,约为一千万分之一,同时也考虑到红黑树查询比链表快;
 

2.3 Set

  • Set的实现

无序集合,不允许有重复值,允许有null值,存入与取出的顺序有可能不一致。

Set判断两个对象相同不是使用==运算符,而是根据equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象。

  • HashSet、LinkedHashSet、TreeHashSet的区别及原理

HashSet
HashSet有以下特点
1.不能保证元素的排列顺序,顺序有可能发生变化
2.不是同步的
3.集合元素可以是null,但只能放入一个null
     当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。

     简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等
     注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其   hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。

TreeSet

    TreeSet类型是J2SE中唯一可实现自动排序的类型

    TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向  TreeSet中加入的应该是同一个类的对象。
    TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
自然排序
    自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
    Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
    obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。
    如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0
定制排序
    自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(To1,To2)方法

LinkedHashSet
     LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
     LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

总结:

  1.  TreeSet 依靠的是Comparable 来区分重复数据;

  2. HashSet 依靠的是hashCode()、equals()来区分重复数据

  3. Set 里面不允许保存重复数据。

3.零散的知识点

封装、继承、多态(面向对象编程的三大特性)

封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。

      继承是为了重用父类代码。两个类若存在IS-A的关系就可以使用继承。,同时继承也为实现多态做了铺垫。那么什么是多态呢?多态的实现机制又是什么?请看我一一为你揭开:

      所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

重载和重写的区别

区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)

final、finally、finalize

final       用于申明属性,方法和类,表示属性不可变,方法不可以被覆盖,类不可以被继承。
finally     是异常处理语句结构中,表示总是执行的部分。  
finallize   表示是object类一个方法,在垃圾回收机制中执行的时候会被调用被回收对象的方法。允许回收此前未回收的内存垃圾。所有object都继承了 finalize()方法

Static关键字

1、static方法

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。

2、static变量

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。

Java程序初始化顺序:

  1. 父类的静态代码块
  2. 子类的静态代码块
  3. 父类的普通代码块
  4. 父类的构造方法
  5. 子类的普通代码块
  6. 子类的构造方法

3、static代码块

static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

i++与++i

i++ :先引用后增加 (先在i所在的表达式中使用i的当前值,后让i加1)
++i :先增加后引用 (让i先加1,然后在i所在的表达式中使用i的新值)

不论是++i还是i++都不是原子操作,在运行中都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。

volatile不能解决这个线程安全问题。因为volatile只能保证可见性,不能保证原子性。

可以用锁。使用synchronized或者ReentrantLock都可以解决这个问题,一般使用synchronized更好,因为JVM团队一直以来都在优先改进这个机制,可以尽早获得更好的性能,并且synchronized对大多数开发人员来说更加熟悉,方便代码的阅读。

可以使用AtomicInteger。AtomicInteger类的底层实现原理是利用处理器的CAS操作(Compare And Swap,比较与交换,一种有名的无锁算法)来检测栈中的值是否被其他线程改变,如果被改变则CAS操作失败。这种实现方法在CPU指令级别实现了原子操作,因此,其比使用synchronized来实现同步效率更高。
(CAS操作过程都包含三个运算符:内存地址V、期望值E、新值N。当操作的时候,如果地址V上存放的值等于期望值E,则将地址V上的值赋为新值N,否则,不做任何操作,但是要返回原值是多少。这就保证比较和设置这两个动作是原子操作。系统主要利用JNI(Java Native Interface,Java本地接口)来保证这个原子操作,它利用CPU硬件支持来完成,使用硬件提供swap和test_and_set指令,但CPU下同一指令的多个指令周期不可中断,SMP(Symmetric Multi-Processing)中通过锁总线支持这两个指令的原子性。)

由于线程共享栈区,不共享堆区和全局区,所以当且仅当 i 位于栈上是安全的,反之不安全。(栈,方法栈,是每个线程方法自己的),AtomicInteger 和 各种 Lock 都可以确保线程安全。AtomicInteger 的效率高是因为它是互斥区非常小,只有一条指令,而 Lock 的互斥区是拿锁到放锁之间的区域,至少三条指令
 

equals()与"=="的区别?

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

重写equals()方法为什么要同时重写hashcode()方法

1.使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率

2.保证是同一个对象,如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。

this和super的区别

(1)代表的事物不同

super代表的是父类空间的引用。

this代表的是所属函数的调用者对象。

 (2)使用前提不同

super必须要有继承关系才能使用

this不需要继承关系也能使用

(3)调用的构造函数不同

super:调用父类的构造函数

this:调用所属类的构造函数

自动装箱与自动拆箱

装箱就是  自动将基本数据类型转换为包装器类型,调用Integer 的 valueOf (int) 方法。

拆箱就是  自动将包装器类型转换为基本数据类型,调用Integer 的 intValue 方法。

异常

 

1.Java异常机制

Java把异常当做对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。Java中的异常分为两大类:错误Error和异常Exception,Java异常体系结构如下图所示:

2.Throwable

Throwable类是所有异常或错误的超类,它有两个子类:Error和Exception,分别表示错误和异常。其中异常Exception分为运行时异常(RuntimeException)和非运行时异常,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

3.Error

一般是指java虚拟机相关的问题,如系统崩溃、虚拟机出错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断,通常应用程序无法处理这些错误,因此应用程序不应该捕获Error对象,也无须在其throws子句中声明该方法抛出任何Error或其子类。

4.运行时异常和非运行时异常

(1)运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

当出现RuntimeException的时候,我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过NullPointerException异常,它就是运行时异常,并且这种异常还是最常见的异常之一。 

出现运行时异常后,如果没有捕获处理这个异常(即没有catch),系统会把异常一直往上层抛,一直到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是Exception的子类,也有一般异常的特点,是可以被catch块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。 

如果不想终止,则必须捕获所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。

(2)非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch并处理,否则程序就不能编译通过。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆catch块去处理可能的异常。

5. 常见RuntimeException:

ArrayStoreException: 试图将错误类型的对象存储到一个对象数组时抛出的异常

ClassCastException: 试图将对象强制转换为不是实例的子类时,抛出该异常

IllegalArgumentExceptio:抛出的异常表明向方法传递了一个不合法或不正确的参数

IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出

NoSuchElementException:表明枚举中没有更多的元素

NullPointerException: 当应用程序试图在需要对象的地方使用 null 时,抛出该异常

Java中throw和throws的区别

1、Throw用于方法内部,Throws用于方法声明上

2、Throw后跟异常对象,Throws后跟异常类型

3、Throw后只能跟一个异常对象,Throws后可以一次声明多种异常类型

发布了16 篇原创文章 · 获赞 12 · 访问量 8088

猜你喜欢

转载自blog.csdn.net/ziyou434/article/details/104949244