当前位置: 首页 > news >正文

Java工程师面试题

1.Java基础

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

JVM(Java虚拟机)是Java跨平台的关键。在程序运行之前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。注意事项:编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;跨平台的是Java程序、而不是JVM,JVM使用C/C++开发的软件,不同平台下需要安装不同版本的JVM。

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

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

1.3.说一说你对Java访问权限的了解

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

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

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

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

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

1.4.介绍一下Java的数据类型

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

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

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

数据类型字节数数据范围
byte1字节(8位)-2^7~2^7-1
short2字节(16位)-2^15~2^15-1
int4字节(32位)-2^31~2^31-1
long8字节(64位)-2^63~2^63-1
float4字节(32位)-3.4*10^38~3.4*10^38
double8字节(64位)-1.8*10^308~1.8*10^308
char2字节(16位)\u0000~\uffff
boolean--

1.5.int类型的数据范围是多少?

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

1.6.请介绍全局变量(成员变量)和局部变量的区别

成员变量:

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

局部变量:

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

1.7.请介绍一下实例变量的默认值

实例变量若为引用数据类型,其默认值一律为null。若为基本数据类型其默认值如下:

  • byte:0
  • short:0
  • int:0
  • long:0L
  • float:0.0F
  • double:0.0
  • char:'\u0000'
  • boolean:false

1.8.为啥要有包装类?

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

1.9.说一说自动装箱、自动拆箱的应用场景

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

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

Integer、Double不能直接进行比较,这包括:

  • 不能用==进行直接比较,因为它们是不同的数据类型;
  • 不能转为字符串进行比较,因为转为字符串后,浮点数带小数点,整数值不带,这样它们永远都不相等;
  • 不能使用compareTo方法进行比较,虽然它们都有compareTo方法,但该方法只能对相同类型进行比较。

整数、浮点数的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为相同的基本数据类型(如:double),然后使用==进行比较。(Integer.doubleValue()==Double.doubleValue())

1.11.int和Integer有什么区别,二者在做==运算会得到什么结果?

int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型。然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。

1.12.说一说你对面向对象的理解

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

1.13.结构化程序设计

结构化程序设计方法主张按功能把软件系统逐步细分,最小的程序单元是函数,每个函数都负责完成一个功能,用以接收一些输入数据,函数对这些输入数据进行处理,处理结束后输出一些数据。两个局限性:设计不够直观,与人类习惯思维不一致;适应性差,可扩展性不强。

1.14.面向对象的三大特征是什么?

封装、继承和多态。其中,封装指的是将对象的实现细节隐藏起来,然后通过一些公用的方法来暴露该对象的功能;继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。

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

封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:

  • 隐藏类的实现细节;
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;
  • 可进行数据检查,从而有利于保证对象信息的完整性;
  • 便于修改,提高代码的可维护性。

封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。

1.16.说一说你对多态的理解

因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。当把一个子类对象直接赋给父类引用变量时,例如BaseClass obj = new SubClass();这个obj引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量,调用同一个方法时呈现出多种不同的行为特征,这就是多态。

1.17.Java中的多态是怎么实现的?

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

1.18.Java为什么是单继承,为什么不能多继承?

首先,Java是单继承,指的是Java中一个类只能有一个直接的父类。Java不能多继承,则是说Java中一个类不能直接继承多个父类。其次,Java在设计时借鉴了C++的语法,而C++是支持多继承的。Java语言之所以摒弃了多继承的这项特征,是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。准确的说,Java是可以实现“多继承”的。因为尽管一个类只能有一个直接父类,但是却可以有任意多个简洁的父类。这样的设计方式,避免了多继承时所产生的的混淆。

1.19.说一说重写与重载的区别

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

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

1.20.构造方法能不能重写?

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

1.21.介绍一下Object类中的方法

Object类提供了如下几个常用方法:

  • Class<?>getClass():返回该对象的运行时类。
  • boolean equals(Object obj):判断指定对象与该对象是否相等。
  • int hashCode():返回该对象的hashCode值。在默认情况下,Object类的hashCode()方法根据该对象的地址来计算。但很多类都重写了Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
  • String toString():返回该对象的字符串表示,当程序使用System.out.println()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。Object类的toString()方法返回运行时类名@十六进制hashCode值格式的字符串,但很多类都重写了Object类的toString()方法,用于返回可以表述该对象信息的字符串。

另外,Object类还提供了wait()、notify()、notifyAll()这几个方法,通过这几个方法可以控制线程的暂停和运行。Object类还提供了一个clone()方法,该方法用于帮助其他对象来实现‘自我克隆’,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于该方法使用了protected修饰,因此它只能被子类重写或调用。

1.22.说一说hashCode()和equals的关系

hashCode()用于获取哈希码(散列码),equals()用于比较两个对象是否相等,它们应遵循如下规定:

  • 如果两个对象相等,则它们必须有相同的哈希码。
  • 如果两个对象有相同的哈希码,则它们未必相等。

在Java中,Set接口代表是无序的、元素不可重复的集合,HashSet则是Set接口的典型实现。HashSet避免重复存储,通过获取对象的哈希码,以及调用对象的equals()方法来解决。

1.23.为什么要重写hashCode()和equals()?

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

1.24.==和equals()有什么区别?

==运算符:

  • 作用于基本数据类型时,是比较两个数值是否相等;
  • 作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;

equals()方法:

  • 没有重写时,Object默认以==来实现,即比较两个对象的内存地址是否形同;
  • 进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。

1.25.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 startWith(String prefix):判断此字符串是否以指定的前缀开头;
  • boolean endsWith(String suffix):判断此字符串是否以指定的后缀结尾;
  • String toUpperCase():将此字符串中所有的字符大写;
  • String toLowerCase():将此字符串所有的字符小写;
  • String replaceFirst(String regex,String replacement):用指定字符串替换第一个匹配的子串;
  • String replaceAll(String regex,String replacement):用指定字符串替换所有的匹配的子串。

String类由final修饰,多以不能被继承。

1.26.说一说String和StringBuffer有什么区别?

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

1.27.说一说StringBuffer和StringBuilder有什么区别

StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。线程安全(加锁、解锁操作)效率低,非线程安全(无加锁、解锁)易产生并发问题,性能高。

1.28.使用字符串时,new和“”推荐使用哪种方式?

先看看“hello”和new String(“hello”)的区别:

  • 当Java程序直接使用“hello”的字符串直接量时,JVM将会使用常量池来管理这个字符串;
  • 当使用new String("hello")时,JVM会先使用常量池来管理“hello”直接量,再调用String类的构造器来创建一个新的String对象被保存在堆内存中。

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

1.29.说一说你对字符串拼接的理解

拼接字符串有很多种方式,其中最常用的有4种,下面列举了这4种方式各自适合的场景。

  • +运算符:如果拼接的都是字符串直接量,则适合使用+运算符实现拼接;
  • StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
  • StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
  • String类的concat方法:如果知识对两个字符串进行拼接,并且包含变量,则适合使用concat方法。

1.30.两个字符串相加的底层是如何实现的?

如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。

1.31.String a = "abc";,说一下这个过程会创建什么,放在哪里?

JVM会使用常量池来管理字符串直接量,在执行这句话时,JVM会先检查常量池是否已经有“abc”,若没有则将“abc”存入常量池,否则就复用常量池中已有的“abc”,将其引用赋值给变量a。

1.32.new String("abc")是去了哪里,仅仅是在堆里面吗?

在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将“abc”存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。

1.33.接口和抽象类有什么区别?

从设计目的上来说,二者有如下的区别:

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

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

从使用方式上来说,二者有如下的区别:

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

共同特征:

  • 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

1.34.接口中可以有构造函数吗?

由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可有包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。

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

接口体现的是一种规范和实现分类的设计哲学,充分利用接口可以极好的降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。

1.36.遇到过异常吗,如何处理?

在Java中,可以按照如下三个步骤处理异常:

  • 捕获异常,将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。
  • 处理异常,在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。
  • 回收资源,如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即无论是否发生异常,finally块内的代码总会被执行。

1.37.说一说Java的异常机制

  • 关于异常处理:在Java中,处理异常的语句由try、catch、finally三部分组成。其中,try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理,若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行。
  • 关于抛出异常:当程序出现错误时,系统会自动抛出异常。除此以外,Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理。
  • 关于异常跟踪栈:程序运行时,经常会发生一系列方法调用,从而形成方法调用栈。异常机制会导致异常在这些方法之间传播,而异常传播的顺序与方法的调用相反。异常从发生异常的方法向外传播,首先传给该方法的调用者,再传给上层调用者,以此类推。最终会传到main方法,若依然没有得到处理,则JVM会终止程序,并打印异常跟踪栈的信息。

1.38.请介绍Java的异常接口

Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。

Error是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。

Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。

1.39.finally是无条件执行的吗?

不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。

注意事项:如果在try块或catch块中使用 System.exit(1); 来退出虚拟机,则finally块将失去执行的机会。但是我们在实际的开发中,重来都不会这样做,所以尽管存在这种导致finally块无法执行的可能,也只是一种可能而已。

1.40.在finally中return会发生什么?

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

1.41.说一说你对static关键字的理解

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

对static关键字而言,有一条非常重要的规则:类成员(包括成员变量、方法、初始化块、内部类和内部枚举)不能访问实例成员(包括成员变量、方法、初始化块、内部类和内部枚举)。因为类成员是属于类的,类成员的作用域比成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

1.42.static修饰的类能不能被继承?

static修饰的类可以被继承。

1.43.static和final有什么区别?

static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征:

  • 类变量:被static修饰的成员变量叫类变量(静态变量)。类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名来访问它。
  • 类方法:被static修饰的成员方法叫类方法(静态方法)。类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它。
  • 静态块:被static修饰的初始化块叫静态初始化块。静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。
  • 静态内部类:被static修饰的内部类叫静态内部类。静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。外部类的所有方法、初始化块都能访问其内部定义的静态内部类。

final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:

  • final类:final关键字修饰的类不可以被继承。
  • final方法:final关键字修饰的方法不可以被重写。
  • final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。

1.44.说一说你对泛型的理解

Java集合有个缺点:把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易的将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

从Java5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如List<String>,表明该List只能保存字符串类型的对象。

有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。

1.45.介绍一下泛型擦除

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个 List<String> 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。

上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:

List<String> list1 = ...; List list2 = list1; // list2将元素当做Object处理

1.46.List<?super T>和List<? extends T>有什么区别?

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

  • List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;

  • List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。

1.47.说一说你对Java反射机制的理解

Java程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如 Person p = new Student(); ,这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。

有时,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息,而解决这个问题有以下两种做法:

  • 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。

  • 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

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

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

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

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

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

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

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

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

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

1.49.说一说Java的四种引用方式

Java对象的四种引用方式分别是强引用、软引用、弱引用、虚引用,具体含义如下:

  • 强引用:这是Java程序中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。

  • 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。

  • 弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。

  • 虚引用:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

2.集合类

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

Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类。这四个接口将集合分成了四大类,其中:

  • Set代表无序的,元素不可重复的集合;
  • List代表有序的,元素可以重复的集合;
  • Queue代表的是先进先出(FIFO)的队列;
  • Map代表具有映射关系(key-value)的集合。

这些接口拥有众多的实现类,其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。

2.2.Java中的容器,线程安全和线程不安全的区别有哪些?

java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的集合类,他们的优点是性能好。如果需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装秤线程安全的集合类。

java.util包下也有线程安全的集合类,例如Vector、HashTable。这些集合类都是比较古老的API,虽然实现了线程安全,但是性能很差。所以即便是需要使用线程安全的集合类,也建议将线程不安全的集合类包装成线程安全的集合类的方式,而不是直接使用这些古来的API。

2.3.Map接口有哪些实现类?

Map接口有很多实现类,其中比较常用的有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。

对于不需要排序的场景,优先考虑使用HashMap,因为它是性能最好的Map实现。如果需要保证线程安全,则可以使用ConcurrentHashMap。它的性能好于Hashtable,因为它在put时采用分段锁/CAS的加锁机制,而不是像Hashtable那样,无论是put还是get都做同步处理。

对于需要排序的场景,如果需要按插入顺序则可以使用LinkedHashMap,如果需要将key按自然顺序排列甚至是自定义排序排列,则可以选择TreeMap,如果需要保证线程安全,则可以使用Collections工具类将上述实现类包装成线程安全的Map。

2.4.描述一下Map put的过程。

HashMap是最经典的Map实现,下面以它的视角介绍put的过程:

1.首次扩容:

        先判断数组是否为空,若数组为空则进行第一次扩容(resize);

2.计算索引:

        通过hash算法,计算键值对在数组中的索引;

3.插入数据:

  • 如果当前位置元素为空,则直接插入数据;
  • 如果当前位置元素非空,且key已存在,则直接覆盖其value;
  • 如果当前位置元素非空,且key不存在,则将数据链到链表末端;
  • 若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;

4.再次扩容:

        如果数组中元素个数(size)超过threshold,则再次进行扩容操作。

2.5.如何得到一个线程安全的Map?

  • 使用Collection工具类,将线程不安全的Map包装成线程安全的Map;
  • 使用java.util.concurrent包下的Map,如ConcurrentHashMap;
  • 不建议使用Hashtable,虽然Hashtable是线程安全的,但是性能较差。

2.6.HashMap有什么特点?

  • HashMap是线程不安全的实现;
  • HashMap可以使用null作为key或value。

2.7.JDK7和JDK8的HashMap有什么区别?

JDK7中的HashMap,是基于数组+链表来实现的,它的底层维护一个Entry数组。JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。

IDE:包括PIO模式和DMA模式,Ultra DMA 33提供了最大33MB/sec的数据传输率,ATA66、ATA100、ATA133分别提供了66MB/sec,100MB/sec和133MB/sec的最大数据传输率。

SATA(Serial ATA):Serial ATA1.0(150MB/sec)、2.0(300MB/sec)、最终(600MB/sec)。

SCSI(Small Computer System Interface):小型计算机系统接口。具有应用范围广、多任务、带宽大、CPU占用率低、以及热插拔等优点。Ultra SCSI:320MB/sec。

SAS(Serial Attached SCSI):串行连接SCSI,是一种新的SCSI技术。采取直接的点到点的串行传输方式,传输速率起步高达300MB/sec,未来可能会达到600MB/sec甚至更多。

2.8.介绍一下HashMap底层的实现原理

它基于hash算法,通过put方法和get方法存储和获取对象。存储对象时,我们将Key和value传给put方法时,它调用key的hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Factor则resize为原来的2倍)。获取对象时,我们将key传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来。在Java8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

2.9.介绍一下HashMap的扩容机制

  • 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模运算(据说提升了5~8倍)
  • 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
  • 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
  • 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,失去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。

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

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

2.11.HashMap为什么用红黑树而不用B树?

B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。

2.12.HashMap为什么线程不安全?

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

2.13.HashMap如何实现线程安全?

  • 直接使用Hashtable类
  • 直接使用ConcurrentHashMap
  • 使用Collections将HashMap包装成线程安全的Map

2.14.HashMap是如何解决哈希冲突的?

为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。

2.15.说一说HashMap和Hashtable的区别

  • Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点。
  • Hashtable不允许使用null作为key和value,如果视图把null值放进Hashtable中,将会引发空指针异常,但HashMap可以使用null作为key或value。

2.16.HashMap与ConcurrentHashMap有什么区别?

HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。

Collections工具类可以将一个Map转换成线程安全的实现,,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。

ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。

2.17.介绍一下ConcurrentHashMap是怎么实现的?

  • JDK1.7:ConcurrentHashMap是由Segment数据结构和HashEntry数组结构构成,采取分段锁来保证安全性。Segment是ReentrantLock重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcuHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry数组,Segment的结构和HashMap类似,是一个数组和链表结构。
  • JDK1.8:摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

2.18.ConcurrentHashMap是怎么分段分组的?

  • get操作:Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成volatile类型。
  • put操作:当执行put操作时,会经历两个步骤:判断是否需要扩容;定位到添加元素的位置,将其放入HashEntry数组中。插入过程会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(尾插法),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

2.19.说一说你对LinkedHashMap的理解

LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。

LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。

LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

2.20.请介绍LinkedHashMap的底层原理

LinkedHashMap继承HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。

相关文章:

  • 网课查题接口使用
  • 算法练习(堆/栈/队列)
  • 大数据-ClickHouse技术一(安装部署)
  • 【Android入门】4、数据持久化:文件、SharedPreferences 和 Sqlite
  • style样式优先级问题【display:block依旧无法显示DOM元素】
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • 面试宝典------经典
  • node.js环境搭建
  • 【5G核心网】手把手教你将Open5gs托管到k8s(KubeSphere)
  • 空城机在CSDN的四周年创作纪念日
  • C++ Reference: Standard C++ Library reference: C Library: clocale: struct lconv
  • JavaSE进阶--集合(2万字总结)
  • CKA考题 [k8s1.21]
  • AcWing第 70 场周赛题解
  • 读FFA-net: Feature Fusion Attention Network for Single Image Dehazing
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • 【腾讯Bugly干货分享】从0到1打造直播 App
  • C++类的相互关联
  • canvas 五子棋游戏
  • Codepen 每日精选(2018-3-25)
  • CSS 三角实现
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • eclipse的离线汉化
  • JS数组方法汇总
  • leetcode-27. Remove Element
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • SQL 难点解决:记录的引用
  • 前端技术周刊 2019-01-14:客户端存储
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 前端之React实战:创建跨平台的项目架构
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • ionic入门之数据绑定显示-1
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • #单片机(TB6600驱动42步进电机)
  • (1) caustics\
  • (2022版)一套教程搞定k8s安装到实战 | RBAC
  • (python)数据结构---字典
  • (附源码)springboot工单管理系统 毕业设计 964158
  • (附源码)ssm高校升本考试管理系统 毕业设计 201631
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (论文阅读40-45)图像描述1
  • (三)elasticsearch 源码之启动流程分析
  • (算法设计与分析)第一章算法概述-习题
  • (一) springboot详细介绍
  • .java 9 找不到符号_java找不到符号
  • .MyFile@waifu.club.wis.mkp勒索病毒数据怎么处理|数据解密恢复
  • .NET/C# 使窗口永不激活(No Activate 永不获得焦点)
  • .net反编译工具
  • .NET设计模式(8):适配器模式(Adapter Pattern)
  • .net生成的类,跨工程调用显示注释
  • .net中生成excel后调整宽度
  • ??myeclipse+tomcat
  • @manytomany 保存后数据被删除_[Windows] 数据恢复软件RStudio v8.14.179675 便携特别版...
  • [04] Android逐帧动画(一)