【2023Java面试八股文】Java基础篇

引言:

本文对多个平台的面试题进行了汇总、分类、概括、整理,对重点进行了标出,更便于阅读和记忆。

【2023Java面试八股文】Java多线程篇_vincewm的博客-CSDN博客

目录

1.请你说说Java的特点和优点,为什么要选择Java?

2.说说你对面向对象的理解

3.请你讲一下Java 8的新特性

4.请你说说Java基本数据类型和引用类型

5.介绍一下包装类的自动拆装箱与自动装箱

6.请你说说==与equals()的区别

7.请你说说hashCode()和equals()的区别,为什么重写equals()就要重写hashcode()

扫描二维码关注公众号,回复: 14825237 查看本文章

8.请你说一下抽象类和接口的区别

9.请介绍一下访问修饰符

10.说说static修饰符的用法

11.请你说一下final关键字

12.请你说说重载和重写的区别,构造方法能不能重写

13.请你说说泛型、泛型擦除

14.请你说说Java的四种引用方式

15.请你说说IO多路复用

16.请你说说BIO、NIO、O

17.请你讲一下Java NIO

18.说说wt()和sleep()的区别

19.请你说说Java的异常处理机制

20.请说说你对反射的了解

21.String常量池

22.请你说说String类

23.String、StringBuffer、Stringbuilder有什么区别

24.请说说你对Java集合的了解

25.请你说说List与Set的区别

26.说说你对ArrayList的理解

27.请你说说ArrayList和LinkedList的区别

28.请你说说HashMap底层原理

29.请你说说ConcurrentHashMap

30.请你说说HashMap和Hashtable的区别


1.请你说说Java的特点和优点,为什么要选择Java?

得分点

Java与C++的区别,Java的优点

标准回答

java语言的特点是:

一次编译,到处运行,即平台无关性;是纯面向对象的语言。适合开发web

JAVA语言的优点有:

  1. 吸收了c++的优点,摒弃了c++多继承和指针的复杂使用。
  2. 具有垃圾回收机制,不需要管理内存。
  3. 面向对象,易于开发和理解。
  4. 跨平台,编译器将Java源代码编译成字节码文件后,JVM将字节码翻译成特定平台的机器码运行程序。Java是跨平台的,JVM不是跨平台的。
  5. 内含大量的库,简化编写工作。
  6. 良好的安全性,java语言提供了一个防止恶意代码攻击的安全机制。
  7. 良好的健壮性。Java有强类型机制、垃圾回收器、异常处理和安全检查机制。
  8. 支持web开发,如servlet,jsp等。

  1. Java是一门非常纯粹的面向对象的编程语言,它在吸收C++语言的各种优点的同时去除了C++语言中令人难以理解的多继承、指针等概念。所以Java语言在保证了强大的功能性的基础上,还比C++语言更为简单易用。
  2. Java语言极好地实现了面向对象理论,是静态面向对象编程语言的代表,它的存在保证了程序员可以用优雅的思维方式进行复杂的编程 。
  3. Java还拥有平台独立性,可以做到"一次编译,到处运行"。java还提供了很多内置的类库,通过这些类库,简化了开发人员的程序设计工作,缩短了项目的开发时间,最重要的是Java提供了垃圾回收器,这也将开发人员从对内存的管理中解脱出来。
  4. 同时Java拥有良好的安全性和健壮性,java语言经常被用在网络环境中,为了增强程序的安全性,java语言提供了一个防止恶意代码攻击的安全机制(数组边界检测和Bytecode校验等)。
  5. java的强类型机制、垃圾回收器、异常处理和安全检查机制使得用java语言编写的程序有很好的健壮性。
  6. 此外,Java还提供了对Web应用开发的支持:例如Applet、Servlet和JSP可以用来开发Web应用程序;Socket、RMI可以用来开发分布式应用程序的类库。

加分回答

Java为什么可以跨平台:

JVM(Java虚拟机)是Java跨平台的关键。 在运行程序之前,Java源代码(.java)需要经过编译器,将源代码翻译成字节码(.class),但字节码不能直接运行,所以必须通过JVM将字节码翻译成特定平台的机器码运行程序。但跨平台的是Java程序、而不是JVM,所以需要在不同平台下安装不同版本的JVM。

2.说说你对面向对象的理解

得分点

封装,继承,多态

标准回答

面向对象的三大基本特征是:封装、继承、多态。

封装:将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,让外部程序通过该类提供的方法来实现对内部信息的操作和访问。提高了代码的可维护性。
继承:实现代码复用的重要手段,通过extends实现类的继承,实现继承的类被称为子类,被继承的类称为父类;
多态:同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作。

实现多态的三个条件:

需有继承关系的存在,需要有方法的重写,需要有父类引用指向子类对象。这里的父类可以是普通的类、抽象类、接口。

封装是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,让外部程序通过该类提供的方法来实现对内部信息的操作和访问。这种做法有助于规范使用者的行为,让使用者只能通过事先预定的方法访问数据,提高了代码的可维护性;

继承是面向对象实现代码复用的重要手段,Java通过extends作为关键字实现类的继承,实现继承的类被称为子类,被继承的类称为父类(有的也被称为基类和超类),父类和子类的关系是一种一般和特殊的关系;

多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。

加分回答

封装优点:

 - 隐藏类的成员变量和实现细节,不允许外部直接访问

 - 规范使用者的行为,让使用者只能通过事先预定的方法访问数据,通过在这个方法中加入逻辑控制,限制使用者对成员变量的不合理访问

 - 可进行数据检查,从而有利于保证对象信息的完整性;

 - 便于修改,提高代码的可维护性

使用继承的优点包括:

 - 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性,提高了代码复用

 - 提高代码的可扩展性,很多开源框架的扩展接口都是通过继承父类来完成的

但同时继承也有很多缺点:

 - 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

 - 降低代码的灵活性,子类必须拥有父类的属性和方法。

 - 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能会导致大段的代码需要重构。 那么多态也有这些优点:

 - 提高了代码的维护性

 - 提高了代码的扩展性

java中实现多态需要三个条件:

1. 需要有继承关系的存在。

2. 需要有方法的重写。

3. 需要有父类的引用指向子类对象。

3.请你讲一下Java 8的新特性

得分点

Lambda表达式、Java8对接口的改进

标准回答

Java8是一个拥有丰富特性的版本,新增了很多特性,这里着重介绍几点:

- Lambda表达式:该特性可以将功能视为方法参数,或者将代码视为数据。使用 Lambda 表达式,可以更简洁地表示单方法接口(称为功能接口)的实例。

- 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与Lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

- 改进了接口:允许在接口中定义默认方法,默认方法必须使用default修饰。

- Stream API:新添加的Stream API(java.util.stream)支持对元素流进行函数式操作。Stream API 集成在 Collections API 中,可以对集合进行批量操作,例如顺序或并行的 map-reduce 转换。

- Date Time API:加强对日期与时间的处理。

4.请你说说Java基本数据类型和引用类型

得分点

Java中数据类型分类、八大数据类型

标准答案

Java的数据类型分为基本数据类型和引用数据类型两大类。

基本数据类型共有八大类,这八大数据类型又可分为四小类,分别是整数类型(byte/short/int/long)、浮点类型(float、double)、字符类型(char)和布尔类型(boolean)。

其中,int是最常用的整数类型,double是最为常用的浮点类型,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。 引用类型包括数组、类、接口类型,还有一种特殊的null类型,所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。 加分回答 对于基本数据类型,你需要了解每种类型所占据的内存空间,这是面试官喜欢追问的问题:

 - 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`。c语言里long占4字节。

 - 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`。c语言里char占1字节。

 - boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。

5.介绍一下包装类的自动拆装箱与自动装箱

得分点

包装类的作用,应用场景

标准回答 

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

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

包装类互相比较方法:不同包装类必须转成相同类型再进行比较,例如Integer转Double和Double比。不能直接进行比较,包括“==”、转字符串后“==”、“compareTo”比较。

应用场景:比如某个方法的形参为包装类型,直接传入基本类型为实参,

自动装箱、自动拆箱是JDK1.5提供的功能。

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法。

加分回答-不同包装类不能直接进行比较

Java是一门非常纯粹的面向对象的编程语言,其设计理念是“一切皆对象”。但8种基本数据类型却不具备对象的特性。Java之所以提供8种基本数据类型,主要是为了照顾程序员的传统习惯。这8种基本数据类型的确带来了一定的方便性,但在某些时候也会受到一些制约。比如,所有的引用类型的变量都继承于Object类,都可以当做Object类型的变量使用,但基本数据类型却不可以。如果某个方法需要Object类型的参数,但实际传入的值却是数字的话,就需要做特殊的处理了。有了包装类,这种问题就可以得以简化。

不同包装类不能直接进行比较,这包括:

- 不能用==进行直接比较,因为它们是不同的数据类型;

- 不能转为字符串进行比较,因为转为字符串后,浮点值带小数点,整数值不带,这样它们永远都不相等;

- 不能使用compareTo方法进行比较,虽然它们都有compareTo方法,但该方法只能对相同类型进行比较。 整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。

6.请你说说==与equals()的区别

得分点

比较基本变量用法,对比引用变量的用法

标准回答

== 比较基本数据类型时,比较的是两个数值是否相等; 比较引用类型是,比较的是对象的内存地址是否相等

equals() 没有重写时,Object默认以==来实现,即比较两个对象的内存地址是否相等; 重写以后,按照对象的内容进行比较

== 是判断对象地址是否相同,equals本质上也是== .但是不同的类对equals做了重写,例如在string类型和Integer类型中把equals改写成了对类容的判断. 

比较基本变量:

==和EQUALS都是JAVA中判断两个变量是否相等的方式,如果判断的是两个基本类型的变量,并且两者都是数值类型(不一定要求数据类型完全相同),只要两个变量的值相等就会返回TRUE。

比较引用变量: 

对于两个引用变量只有他们指向同一个引用时,==才会返回TRUE。==不能用于比较类型上没有父子关系的两个对象。

EQUALS()方法是OBJECT类提供的一个实例方法,所以所有的引用变量都能调用EQUALS()方法来判断他是否与其他引用变量相等,但使用这个方法来判断两个引用对象是否相等的判断标准与使用==运算符没有区别,它同样要求两个引用变量指向同一个对象才会返回TRUE,但如果这样的话EQUALS()方法就没有了存在的意义,所以如果我们希望自定义判断相等的标准时,可以通过重写EQUALS方法来实现。重写EQUALS()方法时,相等条件是由业务要求决定的,因此EQUALS()方法的实现是由业务要求决定的。

//比较地址
//只要new,就在堆内存开辟空间。直接赋值字符串在常量池里。
        String str1 = "hello";        //常量池
        String str2 = "hello";
        String str3 = new String("hello");
        String str4 = new String("hello");
        System.out.println(str1==str2);//true,因为str1和str2指向的是常量池中的同一个内存地址
        System.out.println(str1==str3);//fasle,str1常量池旧地址,str3是new出的新对象,指向一个全新的地址
        System.out.println(str4==str3);//fasle,因为它们引用不同
 
//比较内容
        System.out.println(str4.equals(str3));//true,因为String类的equals方法重写过,比较的是字符串值

7.请你说说hashCode()和equals()的区别,为什么重写equals()就要重写hashcode()

得分点

hashCode()用途,equals()用途,hashCode()、equals()约定

标准回答

用途:

hashCode()方法的主要用途是获取哈希码,equals()主要用来比较两个对象是否相等。

为什么重写equals()就要重写hashcode()? 

因为二者之间有两个约定,相等对象的哈希码也要相等。

如果两个对象相等,它们必须有相同的哈希码;但如果两个对象的哈希码相同,他们却不一定相等。

equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定

如果两个对象相等,它们必须有相同的哈希码;但如果两个对象的哈希码相同,他们却不一定相等。

也就是说,equals()比较两个对象相等时hashCode()一定相等,hashCode()相等的两个对象equqls()不一定相等。

加分回答-equals()和“==”

Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

由于hashCode()与equals()具有联动关系,所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。

8.请你说一下抽象类和接口的区别

得分点

设计目的,接口与抽象类的方法,接口与抽象类的常量与变量,单继承多实现

标准答案

设计目的: 

接口作为系统与外界交互的窗口,体现了一种规范

抽象类作为系统中多个子类的共同父类,它体现的是一种模板式设计,它可以被当作系统实现过程中的中间产品,必须要有更进一步的完善。

相同点:

- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其它类实现和继承

- 接口和抽象类都可以有抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

不同点:

- 接口里只能包含抽象方法和默认方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。

- 接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量

- 接口里不包含构造器;抽象类可以包含构造器,但抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作

- 接口里不能包含初始化块,抽象类则可以包含初始化块

- 单继承多实现:一个类最多只能有一个父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足

总之,接口通常是定义允许多个实现的类型的最佳途径,但当演变的容易性比灵活性和功能更加重要时,应该使用抽象类来定义类型。

加分回答-设计目的

在二者的设计目的上,接口作为系统与外界交互的窗口,体现了一种规范。

对于接口的实现者来说,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

抽象类则不一样,抽象类作为系统中多个子类的共同父类,它体现的是一种模板式设计。抽象类作为多个子类的父类,它可以被当作系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当作最终产品,必须要有更进一步的完善。这种完善可能有几种不同方式。

9.请介绍一下访问修饰符

得分点

private、default、protected、public

标准回答

  • private : 当前类内可见。使用对象:变量、方法。 
  • default : 同包可见。使用对象:类、接口、变量、方法。
  • protected : 同包可见、对其他包下的子类可见。使用对象:变量、方法。 
  • public : 对所有类可见。使用对象:类、接口、变量、方法

Java除了提供的三个访问修饰符分别代表三个访问级别之外还有一个不加修饰符的访问级别,它们访问级别控制从小到大为: private->default->protected->public

他们访问级别分别如下:

private:类中被private修饰的成员只能在当前类的内部被访问。根据这点,我们可以使用它来修饰成员变量,从而将成员变量隐藏在这个类的内部

default:如果类中的成员或者一个外部类不使用任何访问修饰符来进行修饰,那么他就是default级别的,default访问控制的类成员或者外部类可以被相同包下的其他类访问

protected:如果一个类成员被protected访问修饰符修饰,那么这个成员不但可以被同一个包下的其他类访问,还可以被其他包下的子类访问。一般来讲,如果一个方法被protected修饰,那么通常是希望它的子类来重写它。

public:这是Java中最宽松的访问级别,如果类成员被这个修饰符修饰,那么无论访问类和被访问类在不在一个包下,有没有父子关系,这个类成员都可以被访问到。

加分回答

对于局部变量而言,它的作用域就是他所在的方法,不可能被其它类所访问,所以不能使用访问修饰符来修饰。

对于外部类而言,它只有两种控制级别:

public和默认,外部类之所以不能用protected和private修饰,是因为外部类没有处于任何类的内部,所以就没有它所在类的内部,所在类的子类两个范围,protected和private没有意义。使用public声明的外部类可以被所有类引用;不使用访问修饰符创建的外部类只有同一个包内的类能引用。

修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包 public Y Y Y Y Y protected Y Y Y Y/N( 说明) N default Y Y Y N N private Y N N N N

10.说说static修饰符的用法

得分点

static可以修饰什么,static的重要规则

标准答案

static可以修饰什么?

Java类中包含了成员变量、方法、构造器、初始化块和内部类(包括接口、枚举)5种成员,static关键字可以修饰除了构造器外的其他4种成员。

static的重要规则:

被static修饰的成员先于对象存在,所以类成员不能访问实例成员,即静态不能访问非静态,静态中也没有this关键字。this是随着对象的创建存在的。

static关键字修饰的成员被称为类成员。类成员属于整个类,不属于单个对象。 static关键字有一条非常重要的规则,即类成员不能访问实例成员,因为类成员属于类的,类成员的作用域比实例成员的作用域更大,很容易出现类成员初始化完成时,但实例成员还没被初始化,这时如果类成员访问实力成员就会引起大量错误。

static修饰的部分会和类同时被加载。被static修饰的成员先于对象存在,因此,当一个类加载完毕,即使没有创建对象也可以去访问被static修饰的部分。静态比对象优先存在。也就是说,静态可以访问静态,但静态不能访问非静态,而非静态可以访问静态。

静态方法中没有this关键词,因为静态方法是和类同时被加载的,而this是随着对象的创建存在的

11.请你说一下final关键字

得分点

final类,final方法,final变量

标准答案

final关键字可以用来标志其修饰的类不可被继承、方法不可被重写、变量不可变。final修饰的成员变量必须由程序员显示的指定初始值。

当final修饰类时,该类不能被继承,例如java.lang.Math类就是一个final类,它不能被继承。

final修饰的方法不能被重写,如果出于某些原因你不希望子类重写父类的某个方法,就可以用final关键字修饰这个方法。

当final用来修饰变量时,代表该变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值。 final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。

加分回答-final修饰的成员变量必须由程序员显示的指定初始值

对于final修饰的成员变量而言,一旦有了初始值就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块,构造器中为成员变量指定初始值,那么这个成员变量的值将一直是系统默认分配的0、'\u0000'、false或者是null,那么这个成员变量就失去了存在的意义。

所以Java语法规定:final修饰的成员变量必须由程序员显示的指定初始值。

final修饰的实例变量,要么在定义该实例变量时指定初始值,要么在普通初始化块或构造器中为该实例变量指定初始值。

但要注意的是,如果普通初始化块已经为某个实例变量指定了初始值,则不能再在构造器中为该实例变量指定初始值;final修饰的类变量,要么在定义该变量时指定初始值,要么在静态初始化块中为该类变量指定初始值。

实例变量不能在静态初始化块中指定初始值,因为静态初始化块是静态成员,不可以访问实例变量;类变量不能在普通初始化块中指定初始值,因为类变量在类初始化阶段已经被初始化了,普通的初始化块不能为其重新赋值。

系统不会为局部变量进行初始化,所以局部变量必须由程序员显示的初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。如果final修饰的局部变量在定义是没有指定默认值,则可以在后面的代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。

12.请你说说重载和重写的区别,构造方法能不能重写

得分点

重载定义、重写定义

标准回答

重载 

重载要求发生在同一个类中,多个方法之间方法名相同且参数列表不同。

注意:重载与方法返回值和访问修饰符无关。

重写

重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。构造方法不能重写。

另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。若父类方法的访问修饰符为private,则子类不能对其重写。

其实除了二者都发生在方法之间,要求方法名相同之外并没有太大相同点。

加分回答

同一个类中有多个构造器,多个构造器的形参列表不同就被称为构造器重载,构造器重载让Java类包含了多个初始化逻辑,从而允许使用不同的构造器来初始化对象。

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

父类方法和子类方法之间也有可能发生重载,因为子类会获得父类的方法,如果子类中定义了一个与父类方法名字相同但参数列表不同的方法,就会形成子类方法和父类方法的重载。

13.请你说说泛型、泛型擦除

得分点

使用泛型的原因,泛型的概念

标准回答

泛型的本质:将具体的类型参数化,提供了编译时类型安全检测机制

泛型的范围:泛型接口,泛型类,泛型方法。

泛型的好处:

1.可以在编译时检查类型安全。

2.所有的强制转换都是自动和隐式的,可以提高代码的重用率。

泛型擦除:java的泛型是伪泛型,这是因为java在编译期间,所有的泛型类型都会被擦掉,并相应的做出一些类型转换。 

向上转型:

  • `ArrayList<t>`可以向上转型为`List<t>`
  • arrayList<Integer>泛型不可以向上转化为arrayList<number>。因为arrayList<number>接收arrayList<float>,但arrayList< Integer>不可以接收arrayList< float>,不能转回来 

Java在1.5版本中引入了泛型,在没有泛型之前,每次从集合中读取对象都必须进行类型转换,而这么做带来的结果就是:如果有人不小心插入了类型错误的对象,那么在运行时转换处理阶段就会出错。

在提出泛型之后,我们可以告诉编译器集合中接受哪些对象类型。编译器会自动的为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。这使程序变得更加安全更加清楚

加分回答 -向上转型

在Java标准库中的`ArrayList<t>`实现了`List<t>`接口,它可以向上转型为`List<t>`:
 

public class ArrayList<t> implements List<t> { ... }

List<string> list = new ArrayList<string>();

类型`ArrayList<t>`可以向上转型为`List<t>`

注意:

不能把`ArrayList<Integer>`向上转型为`ArrayList<Number>`或`List<Number>`。 这是为什么呢?

假设`ArrayList<Integer>`可以向上转型为`ArrayList<Number>`,观察一下代码:

// 创建ArrayList<integer>类型:
        ArrayList<Integer> integerList = new ArrayList<>(); // 添加一个Integer:
        integerList.add(new Integer(123)); // “向上转型”为ArrayList<number>:
        ArrayList<Number> numberList = integerList; // 添加一个Float,因为Float也是Number:
        numberList.add(new Float(12.34)); // 从ArrayList<integer>获取索引为1的元素(即添加的Float):
        Integer n = integerList.get(1); // ClassCastException!

我们把一个`ArrayList<integer>`转型为`ArrayList<number>`类型后,这个`ArrayList<number>`就可以接受`Float`类型,因为`Float`是`Number`的子类。但是,`ArrayList<number>`实际上和`ArrayList<integer>`是同一个对象,也就是`ArrayList<integer>`类型,它不可能接受`Float`类型, 所以在获取`Integer`的时候将产生`ClassCastException`。

实际上,编译器为了避免这种错误,根本就不允许把`ArrayList<integer>`转型为`ArrayList<number>`。

14.请你说说Java的四种引用方式

得分点

强引用、软引用、弱引用、虚引用

标准回答

强引用:无论什么情况都不会被回收

软引用:内存快溢出时会回收

弱引用:下次垃圾收集器工作时被回收

虚引用:被垃圾收集器回收时能得到系统通知

java中的四种引用方式分别是:

1,强引用,以new关键字创建的引用都是强引用,被强引用引用的对象永远都不会被回收。

2,软引用:以SoftRererence引用对象,被弱引用引用的对象只有在内存空间不足时会被垃圾回收。

3,弱引用,以WeakReference引用对象,被弱引用引用的对象一定会被回收,它只能存活到下一次垃圾回收。

4,虚引用:以PhantomReference引用对象,一个对象被引用引用后不会有任何影响,也无法通过该引用来获取该对象,只是其再被垃圾回收时会收到一个系统通知。

在JDK 1.2版之前,一个对象只有“被引用”或者“未被引用”两种状态,对于描述一些“不太重要”的对象就显得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

弱引用也是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

15.请你说说IO多路复用

得分点

特点(单线程可以处理多个客户端请求)、优势(系统开销小)

标准回答

IO多路复用:把多个I/O的阻塞复用到同一个select的阻塞上,实现单个线程同时操作多个客户端请求。

优势:系统开销小。系统不需要创建、维护额外进程或者线程。

epoll调用

支持I/O多路复用的系统调用有select、pselect、poll、epoll。 

select调用:查询有多少个文件描述符需要进行IO操作,特点:轮询次数多,内存开销大,支持文件描述符的个数有限。

poll调用:和select几乎差不多。但是它的底层数据结构为链表,所以支持文件描述符的个数无上限。

epoll:更加高效的调用方式,底层的数据结构为红黑树加链表。避免大内存分配和轮询。

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。

与传统的多线程/多进程模型比,I/O多路复用的最大优势系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll

加分回答-epoll调用对比select

epoll与select的原理比较类似,为了克服select的缺点,epoll作了很多重大改进:

1. 支持一个进程打开的socket描述符(FD)不受限制 select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统的Apache方案)解决这个问题,不过虽然在Linux上创建进程的代价比较小,但仍旧是不可忽视的,另外,进程间的数据交换非常麻烦,对于Java由于没有共享内存,需要通过Socket通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。值得庆幸的是,epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在1GB内存的机器上大约是10万个句柄左右,具体的值可以通过cat /proc/sys/fs/file- max察看,通常情况下这个值跟系统的内存关系比较大。

2. I/O效率不会随着FD数目的增加而线性下降 传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,由于网络延时或者链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的socket进行操作-这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,那么,只有“活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会。在这点上,epoll实现了一个伪O。针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于活跃态-例如一个高速LAN环境,epoll并不比select/poll效率高太多;相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3. 使用mmap加速内核与用户空间的消息传递 无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存实现。

4. epoll的API更加简单 包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生,关闭epoll描述符等。

16.请你说说BIO、NIO、O

得分点

阻塞IO模型、非阻塞IO模型、异步IO模型

标准回答

BIO:阻塞I/O模型,同步并阻塞,服务实现模式为一个连接对应一个线程,即客户端发送一个连接,服务端要有一个线程来处理,如果连接多了,线程数量不够,就只能等待,即会发生阻塞。

NIO:非阻塞I/O模型,同步非阻塞,服务实现模式为一个线程可以处理多个连接,即客户端发送的连接都会注册到多路复用器上,然后进行轮询连接,有IO请求就处理。

O:异步I/O模型,异步非阻塞,引入了异步通信,采用的是proactor模式,特点是:有效的请求才启动线程,先由操作系统完成再通知服务端。

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别是阻塞I/O模型、非阻塞I/O模型、I/O复用模型、信号驱动I/O模型、异步I/O模型。

BIO、NIO、O这五种模型中的三种,它们分别是阻塞I/O模型、非阻塞I/O模型、异步I/O模型的缩写。

BIO,阻塞I/O模型(blocking I/O):最常用的I/O模型,缺省情形下,所有文件操作都是阻塞的。

我们以套接字接口为例来理解此模型,即在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型。

NIO,非阻塞I/O模型(nonblocking I/O):recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

O,异步I/O模型(asynchronous I/O):告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。

这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作,异步I/O模型由内核通知我们I/O操作何时已经完成。

加分回答

I/O复用模型(I/O multiplexing):Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。

信号驱动I/O模型(signal-driven I/O):首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

17.请你讲一下Java NIO

得分点

Buffer、Channel、Selector

标准回答

NIO:非阻塞I/O模型,同步非阻塞,服务实现模式为一个线程可以处理多个连接,即客户端发送的连接都会注册到多路复用器上,然后进行轮询连接,有IO请求就处理。

新的输入/输出(NIO)库是在JDK 1.4中引入的。NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据。NIO包含三个核心的组件:Buffer(缓冲区)、Channel(通道)、Selector(多路复用器)。 Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。 Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动而且通道可以用于读、写或者同时用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。 Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

加分回答

Java 7的NIO2提供了异步Channel支持,这种异步Channel可以提供更高效的IO,这种基于异步Channel的IO机制也被称为异步IO(AsynchronousIO)。NIO2为O提供了两个接口和三个实现类,其中AsynchronousSocketChannel和AsynchronousServerSocketChannel是支持TCP通信的异步Channel。

18.说说wt()和sleep()的区别

得分点

所属的类型不同、对锁的依赖不同、返回的条件不同

标准回答

wt()和sleep()方法主要有如下三个区别:

1. 所属的类型不同

wt()是Object类的实例方法,调用该方法的线程将进入WTING等待状态。

sleep()是Thread类的静态方法,调用该方法的线程将进入TIMED_WTING超时等待状态。

2. 对锁的依赖不同

 - wt()依赖于synchronized锁,它必须通过监视器进行调用,在调用后线程会释放锁。

 - sleep()不依赖于任何锁,所以在调用后它也不会释放锁。

3. 返回的条件不同

 - 调用wt()进入等待状态的线程,需要由notify()/notifyAll()唤醒,从而返回。

 - 调用sleep()进入超时等待的线程,需要在超时时间到达后自动返回。

加分回答

wt()方法也支持超时参数,线程调用带有超时参数的wt()会进入TIMED_WTING状态,在此状态下的线程可以通过notify()/notifyAll()唤醒从而返回,若在达到超时时间后仍然未被唤醒则自动返回。

如果采用Lock进行线程同步,则不存在同步监视器,此时需要使用Condition的方法实现等待。

Condition对象是通过Lock对象创建出来的,它的awt()方法会导致线程进入WTING状态,它的带超时参数的awt()方法会导致线程进入TIMED_WTING状态,当调用它的signal()/signalAll()方法时,线程会被唤醒从而返回。

19.请你说说Java的异常处理机制

得分点

异常处理、抛出异常、异常跟踪栈

标准回答

Java的异常机制可以分成异常处理、抛出异常和异常跟踪栈的问题三个部分。

异常处理:处理异常的语句由try、catch、finally三部分组成。try块用于包裹业务代码,catch用于捕获并处理某个异常,finally块则用于回收资源。避免在finally块中使用return或throw,防止try-catch里抛异常失效。

抛出异常:throws只能在方法签名中使用,可以抛多个异常,表示出现异常的一种可能性。throw表示抛出一个确定的异常实例。

异常跟踪栈的问题:异常从发生异常的方法向外传播,首先传给该方法的调用者,再传给上层调用者,以此类推。最终会传到mn方法,若依然没有得到处理,则JVM会终止程序,并打印异常跟踪栈的信息。

异常处理机制可以让程序具有极好的容错性和健壮性,当程序运行出现了意料之外的状况时,系统会生成一个Exception对象来通知程序,从而实现“业务功能实现部分代码”与“错误处理部分代码”分离,使程序获得更好的可读性。

Java的异常机制可以分成异常处理、抛出异常和异常跟踪栈问题三个部分。

异常处理 

处理异常的语句由try、catch、finally三部分组成。try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。如果业务代码发生异常,系统就会创建一个异常对象,并将这个异常对象提交给JVM,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。如果JVM没有找到可以处理异常的catch代码块,那么运行环境会终止,Java程序也会退出。若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行(一般情况下)。

抛出异常 

当程序出现错误时,系统会自动抛出异常。除此以外,Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理

异常跟踪栈问题 

程序运行时,经常会发生一系列方法调用,从而形成方法调用栈。异常机制会导致异常在这些方法之间传播,而异常传播的顺序与方法的调用相反。异常从发生异常的方法向外传播,首先传给该方法的调用者,再传给上层调用者,以此类推。最终会传到mn方法,若依然没有得到处理,则JVM会终止程序,并打印异常跟踪栈的信息。

加分回答-throw、throws区别,避免在finally块中使用return或throw

throws:

 - 只能在方法签名中使用

 - 可以声明抛出多个异常,多个一场之间用逗号隔开

 - 表示当前方法不知道如何处理这个异常,这个异常由该方法的调用者处理(如果mn方法也不知该怎么处理异常,这个异常就会交给JVM处理,JVM处理异常的方式是,打印异常跟踪栈信息并终止程序运行,这也就是为什么程序遇到异常会自动结束的的原因)

 - throws表示出现异常的一种可能性,并不一定会发生这些异常

throw:

 - 表示方法内抛出某种异常对象,throw语句可以单独使用。

 - throw语句抛出的是一个异常实例,不是一个异常类,而且每次只能抛出一个异常实例

 - 执行throw一定抛出了某种异常

避免在finally块中使用return或throw

当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句

如果finally块里也使用了return或throw等语句,finally块会终止方法,系统将不会跳回去执行try块、catch块里的任何代码。这将会导致try块、catch块中的return、throw语句失效,所以,我们应该尽量避免在finally块中使用return或throw。

finally代码块不执行的几种情况:

 - 如果当一个线程在执行 try 语句块或者catch语句块时被打断interrupted或者被终止killed,与其相对应的 finally 语句块可能不会执行。

 - 如果在try块或catch块中使用 `System.exit(1);` 来退出虚拟机,则finally块将失去执行的机会。

20.请说说你对反射的了解

得分点

反射概念,通过反射机制可以实现什么,反射应用

反射:在程序运行期间动态的获取对象的属性和方法的功能。

通过反射机制可以实现:

- 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的所有方法和属性

- 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员

- 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象

获取Class对象的三种方式:getClass();xx.class;Class.forName("xxx");

反射的优缺点:

优点:更灵活。运行期间能够动态的获取类,提高代码的灵活性。

缺点:性能差。性能比直接的Java代码要很多。  

应用场景:

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

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

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

aop:

一种编程思想,在不改原有代码的前提下对代码进行增强。

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知内容[如:MyAdvice中的method方法]加进去,就实现了增强,这就是我们所说的代理(Proxy)。

反射

Java程序中,许多对象在运行时都会有编译时异常和运行时异常两种,例如多态情况下Car c = new Audi(); 这行代码运行时会生成一个c变量,在编译时该变量的类型是Car,运行时该变量类型为Audi;另外还有更极端的情况,例如程序在运行时接收到了外部传入的一个对象,这个对象的编译时类型是Object,但程序又需要调用这个对象运行时类型的方法,这种情况下,有两种解决方法:

第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量。第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

具体来说,通过反射机制,我们可以实现如下的操作:

- 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;

- 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员; - 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

加分回答-反射应用场景

Java的反射机制在实际项目中应用广泛,常见的应用场景有:

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

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

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

21.String常量池

常量池:Java虚拟机有一个常量池机制,它会直接把字符串常量放入常量池中,从而实现复用。

Java字符串存储原理: 

  1. 创建字符串常量时,JVM会检查字符串常量池中是否存在这个字符串;
  2. 若字符串常量池中存在该字符串,则直接返回引用实例;若不存在,先实例化该字符串,并且,将该字符串放入字符串常量池中,以便于下次使用时,直接取用,达到缓存快速使用的效果。

str1和str2在赋值时,使用的是字符串常量。 因此str1和str2指向的是常量池中的同一个内存地址,所以返回值是true。

//比较地址
//只要new,就在堆内存开辟空间。直接赋值字符串在常量池里。
        String str1 = "hello";        //常量池里无“hello”对象,创建“hello”对象,str1指向常量池“hello”对象。
//先检查字符串常量池中有没有"hello",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"hello";
        String str2 = "hello";    //常量池里有“hello”对象,str2直接指向常量池“hello”对象。
        String str3 = new String("hello");   //堆中new创建了一个对象。假如“hello”在常量池中不存在,Jvm还会常量池中创建这个对象“hello”。
        String str4 = new String("hello");
        System.out.println(str1==str2);//true,因为str1和str2指向的是常量池中的同一个内存地址
        System.out.println(str1==str3);//fasle,str1常量池旧地址,str3是new出的新对象,指向一个全新的地址
        System.out.println(str4==str3);//fasle,因为它们引用不同
 
//比较内容
        System.out.println(str4.equals(str3));//true,因为String类的equals方法重写过,比较的是字符串值

22.请你说说String类

得分点

String常用方法简单介绍,String能否被继承,创建字符串的两种方式

标准回答

String常用方法: 

String类是Java最常用的API,它包含了大量处理字符串的方法,比较常用的有:

- char charAt(int index):返回指定索引处的字符;

- String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;

- String[] split(String regex):以指定的规则将此字符串分割成数组;

- String trim():删除字符串前导和后置的空格;

- int indexOf(String str):返回子串在此字符串首次出现的索引;

- int lastIndexOf(String str):返回子串在此字符串最后出现的索引;

- boolean startsWith(String prefix):判断此字符串是否以指定的前缀开头;

- boolean endsWith(String suffix):

判断此字符串是否以指定的后缀结尾;

 - String toUpperCase():将此字符串中所有的字符大写;

 - String toLowerCase():将此字符串中所有的字符小写;

 - String replaceFirst(String regex, String replacement):用指定字符串替换第一个匹配的子串;

 - String replaceAll(String regex, String replacement):用指定字符串替换所有的匹配的子串。

String能否被继承: 

String类是由final修饰的,所以他不能被继承。

创建字符串的两种方式: 

创建字符串有两种方式,一种是使用字符串直接量,另一种是使用new+构造器。

当使用字符串直接量的方式来创建字符串时,JVM会使用常量池来管理这个字符串;

当使用new关键字来创建字符串时,JVM会先使用常量池来管理字符串直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象会被保存在堆内存中。

对比来说,采用new的方式会多创建出一个对象来,占用了更多的内存 ,所以建议采用直接量的方式来创建字符串。

直接量是指在程序中通过源代码直接给出的值。 

23.String、StringBuffer、Stringbuilder有什么区别

得分点

字符串是否可变,StringBuffer、StringBuilder线程安全问题

标准回答

String:不可变字符序列,效率低,但是复用率高。

不可变字符序列:String是一个不可变类,一个String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。 String是不可变类,所以是线程安全的.

复用率高: String类型对象创建出来后归常量池管,可以随时从常量池调用同一个String对象。StringBuffer和StringBuider在创建对象后一般要转化成String对象才调用。

StringBuffer和Stringbuilder都是字符序列可变的字符串,方法也一样,有共同的父类AbstractStringBuilder。 

StringBuffer:可变字符序列、效率较高(增删)、线程安全

StringBuilder:可变字符序列、效率最高、线程不安全

Java中提供了String,StringBuffer两个类来封装字符串,并且提供了一系列方法来操作字符串对象。

String是一个不可变类,也就是说,一个String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer对象被创建之后,我们可以通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()、等方法来改变这个字符串对象的字符序列。当通过StringBuffer得到期待中字符序列的字符串时,就可以通过toString()方法将其转换为String对象。

StringBuilder类是JDK1.5中新增的类,他也代表了字符串对象。和StringBuffer类相比,它们有共同的父类`AbstractStringBuilder`,二者无论是构造器还是方法都基本相同,不同的一点是,StringBuilder没有考虑线程安全问题,也正因如此,StringBuilder比StringBuffer性能略高。因此,如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。

24.请说说你对Java集合的了解

得分点

Set、List、Quque、Map

标准回答

Java中的集合类分为4大类,分别由4个接口来代表,它们是Set、List、Queue、Map。其中,Set、List、Queue、都继承自Collection接口。

Set代表无序的、元素不可重复的集合。

List代表有序的、元素可以重复的集合。

Queue代表先进先出(FIFO)的队列。

Map代表具有映射关系(key-value)的集合。

Java提供了众多集合的实现类,它们都是这些接口的直接或间接的实现类,其中比较常用的有:HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。这些集合都是线程不安全的。

线程安全的集合包括:

  1. Collections工具类的synchronizedXxx()方法将ArrayList等集合类包装成线程安全的集合类。
  2.  java.util包下性能差的古老api,如Vector、Hashtable
  3. JUC包下Concurrent开头的、以降低锁粒度来提高并发性能的容器,如ConcurrentHashMap。
  4. JUC包下以CopyOnWrite开头的、采用写时复制技术实现的并发容器,如CopyOnWriteArrayList。
  5. Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。

加分回答

上面所说的集合类的接口或实现,都位于java.util包下,这些实现大多数都是非线程安全的。虽然非线程安全,但是这些类的性能较好。如果需要使用线程安全的集合类,则可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。

java.util包下的集合类中,也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。

从JDK1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。

第一类是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。

第二类是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。

第三类是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。

25.请你说说List与Set的区别

得分点

有序可重复、无序可重复

标准回答

List和Set都是Collection接口的子接口,它们的主要区别在于元素的有序性和重复性

List代表有序的元素可以重复的集合,集合中每个元素都有对应的顺序索引,它默认按元素的添加顺序设置元素的索引,并且可以通过索引来访问指定位置的集合元素。另外,List允许使用重复元素。

Set代表无序的元素不可重复的集合,它通常不能记住元素的添加顺序。Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set,则会引发失败,添加方法将会返回false。

加分回答-TreeSet有序

虽然Set代表无序的集合,但是它有支持排序的实现类,即TreeSet。TreeSet可以确保集合元素处于排序状态,并支持自然排序和定制排序两种排序方式,它的底层是由TreeMap实现的。TreeSet也是非线程安全的,但是它内部元素的值不能为null。

26.说说你对ArrayList的理解

得分点

数组实现、默认容量10、每次扩容1.5倍、缩容、ListIterator

标准回答

数组实现 

ArrayList是基于数组实现的,它的内部封装了一个Object[]数组。 通过默认构造器创建容器时,该数组先被初始化为空数组,之后在首次添加数据时再将其初始化成长度为10的数组。我们也可以使用有参构造器来创建容器,并通过参数来显式指定数组的容量,届时该数组被初始化为指定容量的数组。

每次扩容1.5倍 

如果向ArrayList中添加数据会造成超出数组长度限制,则会触发自动扩容,然后再添加数据。扩容就是数组拷贝,将旧数组中的数据拷贝到新数组里,而新数组的长度为原来长度的1.5倍

手动缩容 

ArrayList支持缩容,但不会自动缩容,即便是ArrayList中只剩下少量数据时也不会主动缩容。如果我们希望缩减ArrayList的容量,则需要自己调用它的trimToSize()方法,届时数组将按照元素的实际个数进行缩减。

加分回答-ListIterator

Set、List、Queue都是Collection的子接口,它们都继承了父接口的iterator()方法,从而具备了迭代的能力。

但是,相比于另外两个接口,List还单独提供了listIterator()方法,增强了迭代能力。iterator()方法返回Iterator迭代器,listIterator()方法返回ListIterator迭代器,并且ListIterator是Iterator的子接口。ListIterator在Iterator的基础上,增加了向前遍历的支持,增加了在迭代过程中修改数据的支持。

27.请你说说ArrayList和LinkedList的区别

得分点

数据结构(数组和链表)、访问效率

标准回答

直接对比数组和链表的空间复杂度、对比插删查的时间复杂度、即可。

1. ArrayList的实现是基于数组,LinkedList的实现是基于双向链表

2. 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。

3. 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。

4. LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 

28.请你说说HashMap底层原理

得分点

数据结构、put()流程、扩容机制

标准回答 

HashMap是线程不安全的,多线程环境下建议使用ConcurrentHashMap。

线程安全的集合类:Collections工具类和concurrent包的集合类。

数据结构

在JDK8中,HashMap底层是采用“数组+链表+红黑树”来实现的。数组用作哈希查找,链表用作链地址法处理冲突,红黑树替换长度为8的链表。

HashMap是基于哈希算法来确定元素的位置(槽)的,当我们向集合中存入数据时,它会计算传入的Key的哈希值,并利用哈希值取余来确定槽的位置。如果元素发生碰撞,也就是这个槽已经存在其他的元素了,则HashMap会通过链表将这些元素组织起来(链地址法处理冲突)。如果碰撞进一步加剧,某个链表的长度达到了8,则HashMap会创建红黑树来代替这个链表,从而提高对这个槽中数据的查找的速度。

自动扩容机制  

HashMap中,数组的默认初始容量为16,这个容量会以2的指数进行扩容,数组存的是链表头结点地址。具体来说,当数组中的元素达到一定比例的时候HashMap就会扩容,这个比例叫做负载因子,默认为0.75

自动扩容机制,是为了保证HashMap初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。

put()流程

put()方法的执行过程中,主要包含四个步骤:

1. 判断数组,若发现数组为空,则进行首次扩容为初始容量16。

2. 判断头节点,若发现头节点为空,则新建链表节点,存入数组。

3. 判断头节点,若发现头节点非空,则将元素插入槽内。

4. 插入元素后,判断元素的个数,若发现超过阈值则以2的指数再次扩容

其中,第3步又可以细分为如下三个小步骤:

1. 若元素的key与头节点一致,则直接覆盖头节点。

2. 若元素为树型节点,则将元素追加到树中。

3. 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表长度以决定是否转为红黑树。若链表长度达到8、数组容量未达到64,则扩容。若链表长度达到8、数组容量达到64,则转为红黑树。

向HashMap中添加数据时,有三个条件会触发它的扩容行为:

1. 如果数组为空,则进行首次扩容。

2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。

3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。 并且,每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。

加分回答-HashMap是非线程安全

HashMap是非线程安全的,在多线程环境下,多个线程同时触发HashMap的改变时,有可能会发生冲突。所以,在多线程环境下不建议使用HashMap,可以考虑使用Collections将HashMap转为线程安全的HashMap,更为推荐的方式则是使用ConcurrentHashMap。

29.请你说说ConcurrentHashMap

得分点

数组+链表+红黑树、锁的粒度

标准答案 

concurrent包的集合类是线程安全的。

ConcurrentHashMap是在JUC(Java.util.concurrent并发包下的一个类,它相当于是一个线程安全的HashMap。 

数组+链表+红黑树:ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”

锁的粒度:采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。

实现机制:

1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换

2. 哈希查找插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的链表头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。

3. 链地址法处理冲突扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。

4. 在扩容的过程中,依然可以支持查找操作。

底层数据结构的逻辑可以参考HashMap的实现,下面我重点介绍它的线程安全的实现机制。

1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS(比较并交换)的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)。

CAS,compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制。

CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。

3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。

4. 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。

加分回答

ConcurrentHashMap实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是 `(数组长度 >>> 3) / CPU核心数`。 也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的。多个线程依据硬件的处理能力,平均分摊一部分槽的迁移工作。另外,如果计算出来的迁移数量小于16,则强制将其改为16,这是考虑到目前服务器领域主流的CPU运行速度,每次处理的任务过少,对于CPU的算力也是一种浪费。 

30.请你说说HashMap和Hashtable的区别

得分点

线程安全、null

标准回答

HashMap和Hashtable都是典型的Map实现,它们的区别在于是否线程安全,是否可以存入null值。

 1. Hashtable在实现Map接口时保证了线程安全性,而HashMap则是非线程安全的。所以,Hashtable的性能不如HashMap,因为为了保证线程安全它牺牲了一些性能

 2. Hashtable不允许存入null,无论是以null作为key或value,都会引发异常。而HashMap是允许存入null的,无论是以null作为key或value,都是可以的。

加分回答-Hashtable是古老api,推荐ConcurrentHashMap

虽然Hashtable是线程安全的,但仍然不建议在多线程环境下使用Hashtable。因为它是一个古老的API,从Java 1.0开始就出现了,它的同步方案还不成熟,性能不好。如果要在多线程环下使用HashMap,建议使用ConcurrentHashMap。它不但保证了线程安全,也通过降低锁的粒度提高了并发访问时的性能。

猜你喜欢

转载自blog.csdn.net/qq_40991313/article/details/129414665