Java八股文(Java基础面试题)

JDK、JRE、JVM 三者之间的关系?

JDK(Java Development Kit):是Java开发工具包,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。它能够创建和编译程序。

JRE(Java Runtime Environment):是Java的运行环境,它是运行已编译Java程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java核心类库,java命令和其他的一些基础构件。

JVM(Java Virtual Machine):是运行Java字节码的虚拟机,是整个Java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。所有的Java程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。

什么是字节码?采用字节码的好处是什么?

JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。

为什么Java代码可以实现一次编写、到处运行?

JVM(Java虚拟机)是Java跨平台的关键。在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

注意:(1)编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;(2)跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。

为什么说 Java 语言“编译与解释并存”?

高级编程语言按照程序的执行方式分为两种:(1)编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。常见的编译性语言有 C、C++、Go等。(2)解释型:解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行。常见的解释性语言有 Python、JavaScript、PHP等等。

Java语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java程序要经过先编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码(.class文件),到执行期时再将字节码直译来解释执行。

一个Java文件里可以有多个类吗(不含内部类)?

一个java文件里可以有多个类,但最多只能有一个被public修饰的类;如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。

什么是面向对象?面向对象和面向过程的区别?

面向对象的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。它从现实世界中客观存在的事物出发来构造软件系统,强调直接以现实世界中的事物为中心来思考,并根据这些事物的本质特点,把它们抽象地表示为系统中的类,作为系统的基本构成单元,这使得软件系统的组件可以直接映像到客观世界,并保持客观世界中事物及其相互关系的本来面貌。

区别:(1)编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。

(2)封装性:都具有封装性,但是面向过程封装的是功能,而面向对象封装的是数据和功能。(3)面向对象具有继承性和多态性,而面向过程没有继承性和多态性。

面向对象的三大特性?

(1)封装:将对象的成员变量和实现细节隐藏起来,不允许外部直接访问,然后通过一些公用方法来控制对这些成员变量进行安全的访问和操作。

(2)继承:继承是从已有的类中得到继承信息后创建新类的过程。提供继承信息的类被称为父类,得到继承信息的类被称为子类,当子类继承父类后将直接获得父类的属性和方法。

(3)多态:分为编译时多态(方法重载)和运行时多态(方法重写)。多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着相同类型的变量在执行同一个方法时可能会表现出多种不同的行为特征。要实现多态需要做两件事:一是子类继承父类并重写父类中的方法,二是用父类型引用子类型对象。

封装的目的是什么,为什么要有封装?

封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以隐藏类的实现细节;让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;可进行数据检查,从而有利于保证对象信息的完整性;便于修改,提高代码的可维护性。

Java的访问权限

Java语言为我们提供了三种访问修饰符,即private、protected、public,在使用这些修饰符修饰目标时,一共可以形成四种访问权限,即private、default、protected、public,注意在不加任何修饰符时为default访问权限。

(1)在修饰类时,该类只有两种访问权限,对应的访问权限的含义如下:

default:该类可以被同一包下其他的类访问;public:该类可以被任意包下,任意的类所访问。

(2)在修饰成员变量/成员方法时,该成员的四种访问权限的含义如下:

private:该成员可以被该类内部成员访问;default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问;protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问;public:该成员可以被任意包下,任意类的成员进行访问。

Java的数据类型

Java数据类型包括基本数据类型和引用数据类型两大类。

基本数据类型有8个,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。在这8个基本类型当中,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。

引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为3类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是Java语言不再沿用指针这个说法而已。

byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1。

short:2字节(16位),数据范围是 -2^15 ~ 2^15-1。

int:4字节(32位),数据范围是 -2^31 ~ 2^31-1。

long:8字节(64位),数据范围是 -2^63 ~ 2^63-1。

float:4字节(32位),数据范围大约是 -3.4*10^38 ~ 3.4*10^38。

double:8字节(64位),数据范围大约是 -1.8*10^308 ~ 1.8*10^308。

char:2字节(16位),数据范围是 \u0000 ~ \uffff。

为啥要有包装类?基本类型和包装类型的区别?

Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。

区别:(1)成员变量包装类型不赋值就是null,而基本类型有默认值且不是null。(2)包装类型可用于泛型,而基本类型不可以。(3)基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被static 修饰)存放在 Java 虚拟机的堆中。包装类型属于对象类型,存在于堆中。(4)相比于对象类型,基本数据类型占用的空间非常小。

装箱和拆箱的区别

自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;

自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。

如何对Integer和Double类型判断相等?

Integer、Double不能直接进行比较,因为它们是不同的数据类型。整数、浮点类型的包装类,都继承于Number类型,所以可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。

Integer和int的区别?

(1)int 是 Java 的八种基本数据类型之一,而Integer是int的包装类;

(2)int型变量的默认值是0,Integer 变量的默认值是null;

(3)Integer变量必须实例化后才可以使用,而int不需要。

注意:①Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true。因为包装类Integer和基本数据类型int类型进行比较时,Integer会自动拆箱为int类型,然后进行比较,实际上就是两个int型变量在进行比较;②由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的,因为其内存地址是不同的;③非new生成的Integer变量和new Integer()生成的变量进行比较时,结果为false。因为非new生成的Integer变量指向的是Java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同。

重载和重写的区别?

重载:重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。

重写:重写发生在子类与父类之间,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。若父类方法的访问修饰符为private,则子类不能对其重写。

构造方法有哪些特点?构造方法能不能重写?

构造方法特点如下:(1)名字与类名相同;(2)没有返回值;(3)生成类的对象时自动执行,无需调用,主要作用是完成对象的初始化工作。

构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。

如果一个类没有声明构造方法,该程序能正确执行吗?

如果一个类没有声明构造方法,也可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java就不会再添加默认的无参数的构造方法了

对象实体与对象引用有何不同?对象的相等和引用相等的区别?

new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

对象的相等一般比较的是内存中存放的内容是否相等;引用相等一般比较的是他们指向的内存地址是否相等。

成员变量和局部变量的区别

成员变量:(1)成员变量是在类的范围里定义的变量;(2)成员变量有默认初始值;(3)未被static修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;(4)被static修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同。

局部变量:(1)局部变量是在方法里定义的变量;(3)局部变量没有默认初始值;(3)局部变量存储于栈内存中,作用的范围结束,变量空间会自动的释放。

静态变量和实例变量的区别?

静态变量:是被static修饰的变量,也称为类变量;当创建了类的多个对象时,多个对象共享同一个静态变量,当修改其中一个对象的静态变量时,其他对象的静态变量也被修改。静态变量随着类的加载而加载,要早于对象的创建,可以通过“类.静态变量”的方式调用。由于类只加载一次,则静态变量在内存中也只会存在一份(在方法区的静态域中)。

实例变量:未被static修饰的成员变量,也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;需要先创建对象,然后通过对象才能访问到它。当创建了类的多个对象时,每个对象独立的拥有一套类中的非静态属性,当修改其中一个对象的非静态属性时,不会导致其他对象中同样的属性被修改。

static关键字

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,static可以修饰类中的所有成员,以static修饰的成员就是类成员,类成员属于整个类,而不属于单个对象。

被static修饰的变量称为静态变量,被static修饰的方法称为静态方法,在静态方法中,只能调用静态的方法或属性,不能访问实例成员。static修饰的成员可以通过类名访问,也可以通过对象名访问。static修饰的类可以被继承。

final关键字

(1)final修饰类:该类不能被继承;(2)final修饰方法:该方法不能被重写

(3)final修饰变量:该变量为常量,final修饰变量时可以赋值的位置有:显示初始化、代码块内初始化、构造方法内初始化。

abstract关键字

abstract修饰类:(1)抽象类不能实例化(不能创造该类的对象);(2)抽象类中可以没有抽象方法;(3)一般都会提供抽象类的子类,通过实例化子类来调用抽象类中的结构

abstarct修饰方法:(1)抽象方法只有方法的声明,没有方法体;(2)包含抽象方法的类一定是抽象类;(3)若子类重写了父类的所有抽象方法,则可以实例化子类,如果子类没有重写父类的所有抽象方法,则需要将子类声明为抽象类。

接口

(1)接口内只能定义全局常量(public static final)、抽象方法(public abstact)、静态方法(public static)、默认方法(public default);

(2)接口内不能定义构造器,因此接口不可以实例化(给接口创建对象);

(3)接口通过类实现(implements)的方式来使用,如果实现类实现了接口中所有的方法,则此实现类可以实例化。

(4)一个类可以实现多个接口(该类需要实现所有接口中的所有方法)

接口和抽象类有什么共同点和区别?

相同:①都不能被实例化;②都可以包含抽象方法;③都可以有默认方法。

不同:①接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法;②接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量;③接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作;④接口里不能包含初始化块;抽象类可以包含初始化块;⑤一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

谈谈你对面向接口编程的理解?

接口体现的是规范和实现分离,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。

hashCode()和equals()

hashCode()的作用是获取哈希码(int整数),哈希码的作用是确定该对象在哈希表中的索引位置。equals()是用于比较两个对象是否相等。如果两个对象的hashCode值相等,那这两个对象不一定相等(哈希碰撞),同样的hashCode有多个对象,继续使用 equals()来判断是否真的相同;如果两个对象的hashCode值相等并且equals()方法也返回true,我们才认为这两个对象相等;如果两个对象的hashCode值不相等,我们就可以直接认为这两个对象不相等。两个相等的对象的hashCode值一定相等。

为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的hashCode值一定相等,也就是说如果equals()方法判断两个对象是相等的,那这两个对象的hashCode值也要相等。如果重写equals()时没有重写hashCode()方法的话就可能会导致equals()方法判断是相等的两个对象,hashCode值却不相等。

==和equals()的区别?

==:(1)对于基本数据类型来说,== 比较的是值;(2)对于引用数据类型来说,== 比较的是对象的内存地址,即判断它们是否为同一个对象。

equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。

(1)类没有重写equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,即比较两个对象的内存地址是否相同。(2)类重写了equals()方法:比较两个对象中的内容是否相等(如String)。

String、StringBuffer、StringBuilder的区别?

String:由final修饰,是不可变类,不能被继承,线程安全。

StringBuffer:可变的字符序列、线程安全。

StringBuilder:可变的字符序列、线程不安全、效率高,和StringBuilder有共同的父类AbstractStringBuilder。

String为什么要设计为不可变类?

(1)由于String被广泛使用,会用来存储敏感信息,如果字符串是可变的,容易被篡改,无法保证使用字符串进行操作时,它是安全的。

(2)字符串常量池的需要,字符串常量池是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;若字符串可变,字符串常量池失去意义。

(3)字符串不变性保证了hash码的唯一性,使得类似HashMap,HashSet等容器才能实现相应的缓存功能。由于String的不可变,避免重复计算hashcode,只要使用缓存的hashcode即可,大大提高了在散列集合中使用String对象的性能。

String字符串修改实现的底层原理?

当用String类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuilder,其次调用StringBuilder的append()方法,最后调用StringBuilder的toString()方法把结果返回。

String str = "abc"和new String("abc")的区别?

String str ="abc":JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址,将其引用赋值给变量;如果字符串常量池中没有该字符串,那么就会在字符串常量池中创建该字符串对象,然后再返回。

new String("abc"):JVM会首先检查字符串常量池中是否已经存在该字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建该字符串对象,然后再到堆内存中再创建一份字符串对象,把字符串常量池中的字符串内容拷贝到内存中的字符串对象中,然后返回堆内存中该字符串的内存地址。

如何处理异常?

在Java中,处理异常的语句由try、catch、finally三部分组成。使用try将可能出现异常的代码包裹起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配。一旦try中的异常对象匹配到某一个catch时,就进入到catch中进行异常处理。处理完成后,就跳出当前的try-catch结构,继续执行后面的代码。像数据库连接、输入输出流、网络变成Socket等资源,JVM不能自动回收,我们需要手动进行资源释放,此时的资源释放需要声明在finally中。finally中声明的是一定会被执行的代码,即使catch中又出现了异常、try中有return语句或catch中有return语句等情况。

Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。如果当前方法不知道该如何处理这个异常,指明执行此方法时可能会出现的异常类型,一旦方法体执行时出现异常,会在异常代码处生成一个异常类对象,此对象满足throws后的异常类型时,就会被抛出,后面的代码不会继续执行,则该异常将交给JVM处理。

finally一定会被执行吗?

finally中声明的是一定会被执行的代码,即使catch中又出现了异常、try中有return语句或catch中有return语句等情况。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。

finally可以有return吗?

在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。

Exception和Error有什么区别?

Error类和Exception类的父类都是Throwable类。主要区别如下:

Exception:程序本身可以处理的异常,可以通过catch来进行捕获。Exception 又可以分为 Checked Exception(受检查异常,必须处理)和 Unchecked Exception (不受检查异常,可以不处理)。

Error:Error属于程序无法处理的错误,例如Java虚拟机运行错误、虚拟机内存不够错误、类定义错误等。这些异常发生时,JVM一般会选择线程终止。

throw和throws的区别?

(1)throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例;

(2)throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。

什么是泛型?有什么作用?

Java的参数化类型被称为泛型,允许程序在创建集合时指定集合元素的类型,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。

泛型的类型必须是类,不能是基本数据类型,如果要用到基本数据类型,使用包装类替换;

泛型擦除

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,与泛型相关的信息会被擦除掉。比如一个List 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。

List和List有什么区别?

? 是类型通配符,List 可以表示各种泛型List的父类,意思是元素类型未知的List;

List 用于设定类型通配符的下限,此处? 代表一个未知的类型,但它必须是T的父类型,即可以接受任何T 的父类构成的 List;

List 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型,即可以接受任何继承自T的类型的List。

什么是反射?

每个类都有一个Class对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class文件,该文件内容保存着Class对象。类加载相当于 Class对象的加载,类在第一次使用时才动态加载到 JVM 中。反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class不存在也可以加载进来。

具体来说,程序运行时,通过反射机制,我们可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;可以通过反射创建任意一个类的实例,并访问该实例的成员;可以通过反射机制生成一个类的动态代理类或动态代理对象。

Java反射在实际项目中有哪些应用场景?

使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;

多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;

面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

continue、break和return的区别是什么?

continue:指跳出当前的这一次循环,继续下一次循环。

break:指跳出整个循环体,继续执行循环下面的语句。

return:用于跳出所在方法,结束该方法的运行。

final、finally、finalize 的区别?

final:用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承;

finally:异常处理语句结构的一部分,表示总是执行;

finallize:Object类的一个方法,在垃圾回收时会调用被回收对象的finalize

如何实现对象的克隆?

(1)实现Cloneable接口并重写Object类中的clone()方法;

(2)实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。

深克隆和浅克隆的区别?

(1)浅克隆:拷贝对象和原始对象的引用类型引用同一个对象。浅克隆只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。

(2)深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象和值都复制过来,会完全复制整个对象,两个对象修改其中任意的值另一个值不会改变。

Java中有哪些容器(集合)?

Java容器主要包括Collection和Map,Collection主要用于存放单一元素; Map主要用于存放键值对。

Collection接口又有三个主要的子接口:List、Set和Queue。(1)List是元素有序、可重复的集合,主要的实现类有ArrayList、LinkedList、Vector;(2)Set是元素无序、不可重复的集合,主要的实现类有HashSet,LinkedHashSet,TreeSet;(3)Queue是先进先出(FIFO)的队列,存储的元素是有序、可重复的,主要的实现类有ArrayQueue、LinkedList、PriorityQueue。

Map用于存储key - value对,是无序的,其中key不能重复,主要的实现类有HashMap、TreeMap、Hashtable、ConcurrentHashMap。

Java容器(集合)的主要实现类有哪些?

ArrayList:线程不安全,效率高;底层使用Object[ ]存储,查找快,增删较慢,随机访问元素的复杂度是O(1)。

LinkedList:线程不安全;底层使用双向链表存储,查找慢、增删快,随机查找元素的复杂度是O(n);LinkedList的节点除了存储数据,还存储了两个指针,比ArrayList更占内存。

Vector:线程安全,效率低;底层使用Object[ ]存储。

HashSet:线程不安全;基于哈希表实现,不支持有序性操作;可以存储null;

TreeSet:线程不安全;基于红黑树实现,支持自然排序和定制排序;不可以存储null;

LinkedHashSet:线程不安全;是HashSet的子类,基于链表和哈希表实现;对于频繁的遍历操作,其效率大于HashSet;

ArrayDeque:基于可变长度的数组和双指针实现,不可以存储null。

PriorityQueue:线程不安全;基于二叉堆结构实现,底层使用可变长的数组来存储数据;可以用它来实现优先队列。

HashMap:线程不安全、效率高;JDK1.7中的HashMap是基于数组+链表来实现的,它的底层维护一个Entry数组;JDK1.8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组;存储的key和value可以是null;

TreeMap:线程不安全;基于红黑树实现,按照key-value对中的key对数据进行排序,实现排序遍历;

Hashtable:线程安全、效率很低;底层采用数组+链表实现,存储的key和value不能是null。

ConcurrentHashMap:线程安全;JDK1.7的ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8采用的数组+链表+红黑树。

LinkedHashMap:作为HashMap的子类;底层使用双向链表存储;

ArrayList实现RandomAccess接口有何作用?为何LinkedList没实现这个接口?

RandomAccess 接口只是一个标志接口,标识实现这个接口的类具有随机访问功能。ArrayList底层是数组,而LinkedList底层是链表。数组天然支持随机访问,时间复杂度为O(1)。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList实现了RandomAccess接口,就表明了他具有快速随机访问功能。

HashSet的实现原理?

HashSet是基于HashMap实现的,HashSet的构造方法中会初始化一个HashMap对象,所有放入HashSet中的集合元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。

Queue与Deque的区别

Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。

Deque 是双端队列,在队列的两端均可以插入或删除元素。

HashMap的put方法的执行过程?

(1)先判断数组是否为空,若数组为空则进行第一次扩容;(2)通过hash算法,计算键值对在数组中的索引;(3)如果当前位置元素为空,则直接插入数据;如果当前位置元素非空,且key已存在,则直接覆盖其value;如果当前位置元素非空,且key不存在,则将数据链到链表末端;(4)若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;(5)如果数组中元素个数(size)超过threshold,则再次进行扩容操作。

HashMap的扩容机制

(1)数组的初始容量为16,而容量是以2的次方扩充的。

(2)数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。

(3)为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),先检测当前数组是否到达一个阈值(64),如果没有到达这个容量,先去扩充数组,否则会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。

HashMap为什么线程不安全?

HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。

HashMap中的循环链表是如何产生的?

在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

ConcurrentHashMap的实现原理?

在jdk 1.7中,ConcurrentHashMap是由Segment数据结构和HashEntry数组结构构成,采取分段锁来保证安全性。Segment继承了ReentrantLock,所以它是一种重入锁,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment 数组,一个Segment里包含一个HashEntry数组,Segment的结构和HashMap类似,是一个数组和链表结构。

JDK1.8 的实现已经摒弃了Segment的概念,而是直接用 Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized 和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。

ConcurrentHashMap是怎么分段分组的?

Segment的get操作先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。

当执行put操作时,首先判断是否需要扩容;然后定位到添加元素的位置,将其放入HashEntry数组中。插入过程会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash 操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(尾插法),会通过继承 ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

HashTable和ConcurrentHashMap的区别?

HashTable 和 ConcurrentHashMap 相比,效率低。

Hashtable之所以效率低主要是使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整张Hash表加锁,即每次锁住整张表让线程独占,致使效率低下;

ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可,因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。

HashMap的get方法的执行过程?能否判断元素是否在map 中?

通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

不能判断一个key是否包含在map中,因为get返回null有可能是不包含该 key,也有可能该key对应的value为 null。因为HashMap中允许key为null,也允许value为 null。

JDK1.8之后,为什么HashMap头插法改为尾插法?

头插法在并发下时可能形成数据环,get数据时死循环,而在1.8之前因为处理 hash 冲突的方式是用链表存放数据,使用头插法可以提升一定效率。但是在 1.8 之后链表长度达到阈值就要考虑升级红黑树了,所以哪怕进行尾插遍历次数也会很有限,效率影响不大。

Iterator

迭代器可以遍历并选择序列中的对象,并且只能单向移动,而开发人员不需要了解该序列的底层结构。

iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。之后使用next()获得序列中的下一个元素。 

Iterator和ListIterator的区别?

Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。

Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。

ListIterator 实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。

Java中的IO流

数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件)的过程即输出。

按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。

按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。字符流通常处理文本文件,字节流用来处理图片、视频、音频等非文本文件。

InputStream:字节输入流;Reader:字符输入流

OutputStream:字节输出流;Writer:字符输出流

Java八股文(Java基础面试题)_第1张图片

怎么用流打开一个大文件?

(1)使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数。使用缓冲输入流时,它每次会读取一批数据将缓冲区填满,每次调用读取方法并不是直接从设备取值,而是从缓冲区取值,当缓冲区为空时,它会再一次读取数据,将缓冲区填满。使用缓冲输出流时,每次调用写入方法并不是直接写入到设备,而是写入缓冲区,当缓冲区填满时它会自动写入设备。

(2)使用NIO。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,通过这种方式来进行输入/输出比传统的输入/输出要快得多。

Java中3种常见IO模型

(1)BIO:BIO属于同步阻塞IO模型,同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

(2)NIO:NIO主要由Channel,Buffer,Selector组成。它是支持面向缓冲的,基于通道的I/O操作方法。所有的IO在NIO中都从一个Channel开始,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。Buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。Selector允许单线程处理多个Channel,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

(3)AIO:异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

Java的序列化与反序列化

序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize)是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。

若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的。

若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。

Serializable接口为什么需要定义serialVersionUID变量?

serialVersionUID代表序列化的版本,通过定义类的序列化版本,在反序列化时,只要对象中所存的版本和当前类的版本一致,就允许做恢复数据的操作,否则将会抛出序列化版本不一致的错误。

如果不定义序列化版本,在反序列化时可能出现冲突的情况。增加了序列化版本之后,在这种情况下则可以抛出异常,以提示这种矛盾的存在,提高数据的安全性。

除了Java自带的序列化之外,你还了解哪些序列化工具?

Json:简单直观,可读性好,有jackson,gson,fastjson等解析工具

Protobuf:用来序列化结构化数据的技术,可以用来持久化数据或者序列化成网络传输的数据。更加节省空间(以二进制流存储)、速度更快、更加灵活。

Thrift:不仅仅是序列化协议,而是一个RPC框架,能够满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。

Avro:提供两种序列化格式,即JSON格式或者Binary格式。

你可能感兴趣的:(面试题,java)