Java工程师面试题-Java语言-Java基础

推荐先阅读Java工程师面试题

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

参考答案

JVM(Java虚拟机)是Java跨平台的关键。

在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。

同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

注意事项

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

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

参考答案

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

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

参考答案

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

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

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

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

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

补充

介绍一下Java的数据类型

参考答案

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

基本数据类型有8个,可以分为4个小类,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。其中,4个整数类型中,int类型最为常用。2个浮点类型中,double最为常用。另外,在这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.410^38 ~ 3.410^38。
  • double:8字节(64位),数据范围大约是 -1.810^308 ~ 1.810^308。
  • char:2字节(16位),数据范围是 \u0000 ~ \uffff。
  • boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。

对于引用数据类型,你需要了解JVM的内存分布情况,知道引用以及引用对象存放的位置,详见JVM部分的题目。

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

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

请介绍全局变量和局部变量的区别

参考答案

Java中的变量分为成员变量和局部变量,它们的区别如下:

成员变量:

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

局部变量:

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

注意事项

Java中没有真正的全局变量,面试官应该是出于其他语言的习惯说全局变量的,他的本意应该是指成员变量。

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

参考答案

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

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

注意事项

上述默认值规则适用于所有的成员变量,所以对于类变量也是适用的。

为啥要有包装类?

参考答案

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

扩展阅读

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

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

参考答案

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

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

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

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

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

参考答案

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

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

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

示例代码

class class1 {
void method1() {
Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());
}
}

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

参考答案

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

补充

1.把基本数据类型的值转成字符串

Integer it = 100 ;

  • 调用toString()方法。String itStr = it.toString();
  • 调用Integer.toString(基本数据类型的值)得到字符串。String itStr1 = Integer.toString(it);
  • 直接把基本数据类型+空字符串就得到了字符串。String itStr2 = it+"";

2.把字符串类型的数值转换成对应的基本数据类型的值。(真的很有用)

String numStr = “23”;

  • int numInt = Integer.parseInt(numStr);
  • int numInt = Integer.valueOf(numStr);

String doubleStr = “99.9”;

  • double doubleDb = Double.parseDouble(doubleStr);
  • double doubleDb = Double.valueOf(doubleStr);

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

参考答案

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

扩展阅读

结构化程序设计方法主张按功能来分析系统需求,其主要原则可概括为自顶向下、逐步求精、模块化等。结构化程序设计首先采用结构化分析方法对系统进行需求分析,然后使用结构化设计方法对系统进行概要设计、详细设计,最后采用结构化编程方法来实现系统。

因为结构化程序设计方法主张按功能把软件系统逐步细分,因此这种方法也被称为面向功能的程序设计方法;结构化程序设计的每个功能都负责对数据进行一次处理,每个功能都接受一些数据,处理完后输出一些数据,这种处理方式也被称为面向数据流的处理方式。

结构化程序设计里最小的程序单元是函数,每个函数都负责完成一个功能,用以接收一些输入数据,函数对这些输入数据进行处理,处理结束后输出一些数据。整个软件系统由一个个函数组成,其中作为程序入口的函数被称为主函数,主函数依次调用其他普通函数,普通函数之间依次调用,从而完成整个软件系统的功能。

每个函数都是具有输入、输出的子系统,函数的输入数据包括函数形参、全局变量和常量等,函数的输出数据包括函数返回值以及传出参数等。结构化程序设计方式有如下两个局限性:

  • 设计不够直观,与人类习惯思维不一致。采用结构化程序分析、设计时,开发者需要将客观世界模型分解成一个个功能,每个功能用以完成一定的数据处理。
  • 适应性差,可扩展性不强。由于结构化设计采用自顶向下的设计方式,所以当用户的需求发生改变,或需要修改现有的实现方式时,都需要自顶向下地修改模块结构,这种方式的维护成本相当高。

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

参考答案

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

扩展阅读

抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是考虑部分问题。例如,需要考察Person对象时,不可能在程序中把Person的所有细节都定义出来,通常只能定义Person的部分数据、部分行为特征,而这些数据、行为特征是软件系统所关心的部分。

补充:继承

子类不能继承父类的构造器:子类有自己的构造器。

子类是否可以继承父类的私有成员(私有成员变量,私有成员方法)? 需要暴力访问。

子类是否可以继承父类的静态成员?共享并非继承。

继承后-成员变量的访问特点:就近原则

继承后-成员方法的访问特点:就近原则

方法重写

  • 方法签名相同(申明不变,重新实现)
  • 两小一大
    • 返回值类型:子类≤父类
    • 抛出的异常:子类≤父类
    • 权限修饰符:子类≥父类
  • @Override优势:可读性好,安全,优雅!!
  • super可以用在子类的实例方法中调用父类被重写的方法。
  • 静态方法和私有方法可以被重写(拓展语法)

继承后-构造器的特点

  • 子类的全部构造器默认一定会先访问父类的无参数构造器,再执行子类自己的构造器。

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

参考答案

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

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

扩展阅读

为了实现良好的封装,需要从两个方面考虑:

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问;
  • 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用Java提供的访问控制符来实现。

说一说你对多态的理解

参考答案

因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。

当把一个子类对象直接赋给父类引用变量时,例如 BaseClass obj = new SubClass()
;,这个obj引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。

扩展阅读

多态可以提高程序的可扩展性,在设计程序时让代码更加简洁而优雅。

例如我要设计一个司机类,他可以开轿车、巴士、卡车等等,示例代码如下:

class Driver {
void drive(Car car) {
//...
}

void drive(Bus bus) {
//...
}

void drive(Truck truck) {
//...
}
}

在设计上述代码时,我已采用了重载机制,将方法名进行了统一。这样在进行调用时,无论要开什么交通工具,都是通过driver.drive(obj)这样的方式来调用,对调用者足够的友好。

但对于程序的开发者来说,这显得繁琐,因为实际上这个司机可以驾驶更多的交通工具。当系统需要为这个司机增加车型时,开发者就需要相应的增加driver方法,类似的代码会堆积的越来越多,显得臃肿。

采用多态的方式来设计上述程序,就会变得简洁很多。我们可以为所有的交通工具定义一个父类Vehicle,然后按照如下的方式设计drive方法。调用时,我们可以传入Vehicle类型的实例,也可以传入任意的Vehicle子类型的实例,对于调用者来说一样的方便,但对于开发者来说,代码却变得十分的简洁了。

class Driver {
void drive(Vehicle vehicle) {
// ...
}
}

补充

class Animal {
public void run() {
System.out.println("动物跑!");
}
}

class Dog extends Animal {
@Override
public void run() {
System.out.println("🐕跑的贼快~~~~!");
}

// 独有功能
public void lookDoor() {
System.out.println("🐶看门");
}
}

class Cat extends Animal {
@Override
public void run() {
System.out.println("🐱跑的飞快~~~~!");
}

// 独有功能
public void catchMouse() {
System.out.println("🐱抓🐀");
}
}

class Wolf extends Animal {
@Override
public void run() {
System.out.println("狼跑的飞快~~~");
}

public void catchSheep() {
System.out.println("🐺抓🐏");
}
}
  • 父类类型 对象名称 = new 子类构造器;

  • 接口 对象名称 = new 实现类构造器;

  • 父类类型的范围 > 子类类型范围的。

  • 多态的识别技巧: 对于方法的调用:编译看左边,运行看右边。 对于变量的调用:编译看左边,运行看左边。

  class class1 {
viod method() {
Animal dlam = new Dog();
dlam.run(); // 对于方法的调用:编译看左边,运行看右边。
dlam.lookDoor(); // 报错了,多态形式下,编译看左边,左边没有独有功能
}
}
  • 多态的使用前提: (1) 必须存在继承或者实现关系。 (2) 必须存在父类类型的变量引用子类类型的对象。 (3) 需要存在方法重写。

  • 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变, 便于扩展和维护。可以实现类与类之间的解耦。

  • 实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法, 可以传入一切子类对象进行方法的调用,更能体现出多态的扩展性与便利。

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

参考答案

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

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

参考答案

首先,Java是单继承的,指的是Java中一个类只能有一个直接的父类。Java不能多继承,则是说Java中一个类不能直接继承多个父类。

其次,Java在设计时借鉴了C++的语法,而C++是支持多继承的。Java语言之所以摒弃了多继承的这项特征,是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。

准确来说,Java是可以实现”多继承”的。因为尽管一个类只能有一个直接父类,但是却可以有任意多个间接的父类。这样的设计方式,避免了多继承时所产生的混淆。

说一说重写与重载的区别

参考答案

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

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

构造方法能不能重写?

参考答案

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

介绍一下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修饰,因此它只能被子类重写或调用。

扩展阅读

Object类还提供了一个finalize()方法,当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。并且,针对某一个对象,垃圾回收器最多只会调用它的finalize()方法一次。

注意,finalize()方法何时调用、是否调用都是不确定的,我们也不要主动调用finalize()方法。从JDK9开始,这个方法被标记为不推荐使用的方法。

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

参考答案

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

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

扩展阅读

在Java中,Set接口代表无序的、元素不可重复的集合,HashSet则是Set接口的典型实现。

当向HashSet中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。实际上,HashSet是通过获取对象的哈希码,以及调用对象的equals()
方法来解决这个判断问题的。

HashSet首先会调用对象的hashCode()方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则HashSet会调用equals()
对两个对象进行比较。若相等则说明对象重复,此时不会保存新加的对象。若不等说明对象不重复,但是它们存储的位置发生了碰撞,此时HashSet会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。之后,再有新添加对象也映射到这个位置时,就需要与这个位置中所有的对象进行equals()
比较,若均不相等则将其链到最后一个对象之后。

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

参考答案

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

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

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

参考答案

==运算符:

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

equals()方法:

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

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可以被继承吗?

参考答案

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

扩展阅读

在Java中,String类被设计为不可变类,主要表现在它保存字符串的成员变量是final的。

  • Java 9之前字符串采用char[]数组来保存字符,即 private final char[] value;
  • Java 9做了改进,采用byte[]数组来保存字符,即 private final byte[] value;

之所以要把String类设计为不可变类,主要是出于安全和性能的考虑,可归纳为如下4点。

  • 由于字符串无论在任何 Java 系统中都广泛使用,会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景里,保证字符串 String
    类的安全性就尤为重要了,如果字符串是可变的,容易被篡改,那我们就无法保证使用字符串进行操作时,它是安全的,很有可能出现 SQL 注入,访问危险文件等操作。
  • 在多线程中,只有不变的对象和值是线程安全的,可以在多个线程中共享数据。由于 String
    天然的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。
  • 字符串作为基础的数据结构,大量地应用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根据对象的 hashCode() 方法来确定元素的位置。由于字符串 hashcode 属性不会变更,保证了唯一性,使得类似
    HashMap,HashSet 等容器才能实现相应的缓存功能。由于 String 的不可变,避免重复计算 hashcode,只要使用缓存的 hashcode 即可,这样一来大大提高了在散列集合中使用 String 对象的性能。
  • 当字符串不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的 String.intern()
    方法也失效,每次创建新的字符串将在堆内开辟出新的空间,占据更多的内存。

因为要保证String类的不可变,那么将这个类定义为final的就很容易理解了。如果没有final修饰,那么就会存在String的子类,这些子类可以重写String类的方法,强行改变字符串的值,这便违背了String类设计的初衷。

说一说String和StringBuffer有什么区别

参考答案

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

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

说一说StringBuffer和StringBuilder有什么区别

参考答案

StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类
AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

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

参考答案

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

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

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

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

参考答案

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

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

扩展阅读

采用 + 运算符拼接字符串时:

  • 如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的,所以效率非常的高。
  • 如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()
    方法,将这些字符串拼接在一起,效率也很高。但如果这个拼接操作是在循环中进行的,那么每次循环编译器都会创建一个StringBuilder实例,再去拼接字符串,相当于执行了 new StringBuilder().append(str)
    ,所以此时效率很低。

采用StringBuilder/StringBuffer拼接字符串时:

  • StringBuilder/StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。当拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,即缓冲区加倍。
  • 缓冲区频繁的扩容会降低拼接的性能,所以如果能提前预估最终字符串的长度,则建议在创建可变字符串对象时,放弃使用默认的容量,可以指定缓冲区的容量为预估的字符串的长度。

采用String类的concat方法拼接字符串时:

  • concat方法的拼接逻辑是,先创建一个足以容纳待拼接的两个字符串的字节数组,然后先后将两个字符串拼到这个数组里,最后将此数组转换为字符串。
  • 在拼接大量字符串的时候,concat方法的效率低于StringBuilder。但是只拼接2个字符串时,concat方法的效率要优于StringBuilder。并且这种拼接方式代码简洁,所以只拼2个字符串时建议优先选择concat方法。

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

参考答案

如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。

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

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

参考答案

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

new String(“abc”) 是去了哪里,仅仅是在堆里面吗?

参考答案

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

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

参考答案

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

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

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

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

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

扩展阅读

接口和抽象类很像,它们都具有如下共同的特征:

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

补充

设计目的区别

  • 接口的设计目的:只约束行为有无, 但不限制实现。
  • 抽象类的设计目的:代码复用。
  • 抽象类是用来捕捉子类的通用特性的,而接口则是抽象方法的集合

使用场景

  • 当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为单继承。在这个类中,你必须继承或编写出其所有子类的所有共性。

虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度

  1. 在接口中常量的修饰符:public static final 可以省略不写,默认会加上。

  2. 在接口中抽象方法的修饰符:public abstract 可以省略不写,默认会加上。

  3. JDK 1.8开始之后接口新增了如下三种方法。

    a.默认方法(就是之前写的普通实例方法)

    • 必须用default修饰,默认会public修饰
    • 必须用接口的实现类的对象来调用。

    b.静态方法

    • 默认会public修饰
    • 注意:接口的静态方法必须用接口的类名本身来调用。

    c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。

    • 只能在本类中被其他的默认方法或者私有方法访问。
  4. 实现多个接口的使用注意实现。(非常偏的语法,理解和了解即可)

    • 如果实现了多个接口,多个接口中存在同名的静态方法并不会冲突, 原因是只能通过各自接口名访问静态方法。
    • 当一个类,既继承一个父类,又实现若干个接口时,(重点)
      父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
    • 当一个类实现多个接口时,多个接口中存在同名的默认方法。 实现类必须重写这个方法。
    • 接口中,没有构造器,不能创建对象。(重点)
      接口是更彻底的抽象,连构造器都没有,自然不能创建对象!!

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

参考答案

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

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

参考答案

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

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

参考答案

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

  1. 捕获异常

    将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。

  2. 处理异常

    在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。

  3. 回收资源

    如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即无论是否发生异常,finally块内的代码总会被执行。

说一说Java的异常机制

参考答案

关于异常处理:

在Java中,处理异常的语句由try、catch、finally三部分组成。其中,try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行。

关于抛出异常:

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

关于异常跟踪栈:

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

请介绍Java的异常接口

参考答案

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

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

Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

补充

迭代器遍历没有此元素异常:NoSuchElementException。 数字转换异常: NumberFormatException。

异常的产生默认的处理过程解析

目标:异常的产生默认的处理过程解析。(自动处理的过程!)
(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。 (2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。 (3)虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
(4)直接从当前执行的异常点干掉当前程序。 (5)后续代码没有机会执行了,因为程序已经死亡。

编译时异常的处理机制

监视捕获处理异常企业级写法:

try {
// 可能出现异常的代码!
} catch (Exception e) {
e.printStackTrace(); // 直接打印异常栈信息
}

Exception可以捕获处理一切异常类型!

规范做法:抛出最外层统一处理

异常的语法注意

  • 运行时异常被抛出可以不处理。可以自动抛出,编译时异常必须处理.按照规范都应该处理!
  • 重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小
  • 方法默认都可以自动抛出运行时异常! throws RuntimeException可以省略不写!!
  • 当多异常处理时,捕获处理,前边的异常类不能是后边异常类的父类。
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。

自定义异常

/**
自定义编译时异常类:
1.继承Exception。
2.重写构造器。
*/
public class ItheimaAgeIllegalException extends Exception {
public ItheimaAgeIllegalException() {
}

public ItheimaAgeIllegalException(String message) {
super(message);
}

public ItheimaAgeIllegalException(String message, Throwable cause) {
super(message, cause);
}

public ItheimaAgeIllegalException(Throwable cause) {
super(cause);
}

public ItheimaAgeIllegalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/**
自定义运行时异常类:
1.继承RuntimeException
2.重写构造器。
*/
public class ItheimaAgeIllegalRuntimeException extends RuntimeException {
public ItheimaAgeIllegalRuntimeException() {
}

public ItheimaAgeIllegalRuntimeException(String message) {
super(message);
}

public ItheimaAgeIllegalRuntimeException(String message, Throwable cause) {
super(message, cause);
}

public ItheimaAgeIllegalRuntimeException(Throwable cause) {
super(cause);
}

public ItheimaAgeIllegalRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

异常分类以及处理机制

java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。Throwable又派生出Error类和Exception类。

错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被java异常处理机制使用, 是异常处理的核心。

总体上我们根据 javac 对异常的处理要求,将异常类分为二类。

非检查异常( unckecked exception )

Error 和 RuntimeException 以及他们的子类。 javac 在编译时, 不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用 try… catch…finally )这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处 理 。这样的异常发生的原因多半是代码写的有问题。如除0错误 ArithmeticException ,错误的强制类型转换错 误 ClassCastException ,数组索引越界 ArrayIndexOutOfBoundsException ,使用了空对象 NullPointerException 等等。

检查异常( checked exception )

除了 Error 和 RuntimeException 的其它异常。 javac 强制要求程序员 为这样的异常做预备处理工作(使用 try…catch…finally 或者 throws )。在方法中要么用 try-catch 语句捕 获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因 为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样 的异常时刻准备着。如 SQLException , IOException , ClassNotFoundException 等。

需要明确的是:检查和非检查是对于 javac 来说的,这样就很好理解和区分了。

finally是无条件执行的吗?

参考答案

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

注意事项

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

在finally中return会发生什么?

参考答案

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

详细解析

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

补充

  • finally的作用: 可以在代码执行完毕以后进行资源的释放操作。 什么是资源?资源都是实现了Closeable接口的,都自带close()关闭方法!!
  • try: 1次。 catch:0-N次 (如果有finally那么catch可以没有!!)
    finally: 0-1次
class class1 {
public static void chu() {
InputStream is = null;
try {
is = new FileInputStream("D:/cang.png");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 回收资源。用于在代码执行完毕以后进行资源的回收操作!
try {
if (is != null) is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

从 java 7 build 105 版本开始,java 7 的编译器和运行环境支持新的 try-with-resources 语句,称为 ARM 块(Automatic Resource Management) ,自动资源管理。

class class1 {
private static void customBufferStreamCopy(File source, File target) {
try (InputStream fis = new FileInputStream(source);
OutputStream fos = new FileOutputStream(target)) {
byte[] buf = new byte[8192];
int i;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

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

参考答案

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

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

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

参考答案

static修饰的类可以被继承。

扩展阅读

如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。

static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。

静态内部类需满足如下规则:

  1. 静态内部类可以包含静态成员,也可以包含非静态成员;

  2. 静态内部类不能访问外部类的实例成员,只能访问它的静态成员;

  3. 外部类的所有方法、初始化块都能访问其内部定义的静态内部类;

  4. 在外部类的外部,也可以实例化静态内部类,语法如下:

    外部类.内部类 变量名 = new 外部类.内部类构造方法();

static和final有什么区别?

参考答案

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

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

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

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

扩展阅读

变量分为成员变量、局部变量。

final修饰成员变量:

  • 类变量:可以在声明变量时指定初始值,也可以在静态初始化块中指定初始值;
  • 实例变量:可以在声明变量时指定初始值,也可以在初始化块或构造方法中指定初始值;

final修饰局部变量:

  • 可以在声明变量时指定初始值,也可以在后面的代码中指定初始值。

注意:被 final 修饰的任何形式的变量,一旦获得了初始值,就不可以被修改!

补充

final和abstract的关系:互斥关系,不能同时修饰类或者同时修饰方法!!

修饰基本数据类型的变量,数值初始化之后不可更改。修饰引用类型的变量,引用不可变,引用的值可变。

final int[] iArr = {1, 2, 3, 4};
iArr[2] = -3;//合法
iArr = null;//非法,对iArr不能重新赋值

final Person p = new Person(25);
p.setAge(24);//合法
p = null;//非法

为什么局部内部类和匿名内部类只能访问局部final变量?

  • 编译之后会生成两个class文件,Test.class Test1.class
public class Test {
public static void main(String[] args) {
}

//局部final变量a,b
public void test(final int b) {
//jdk8在这里做了优化, 不用写,语法糖,但实际上也是有的,也不能修改
final int a = 10;

//匿名内部类
new Thread() {
public void run() {
System.out.println(a);
System.out.println(b);
}
}.start();
}
}

class OutClass {
private int age = 12;

public void outPrint(final int x) {
class InClass {
public void InPrint() {
System.out.println(x);
System.out.println(age);
}
}
new InClass().InPrint();
}
}

首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着 方法的执行完毕就被销毁。

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有 没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解
决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的”copy”。这样就好像延长了局部变量的生命周期

将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修 改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量 和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

补充:finalize

finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize()
,回收垃圾,一个对象是否可回收的最后判断。

说一说你对泛型的理解

参考答案

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

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

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

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

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

补充

泛型接口

修饰符 interface 接口名称<泛型变量>{

}
public interface Data<E> {
void add(E stu);

void delete(E stu);

void update(E stu);

E query(int id);
}

// 操作学生数据
public class StudentData implements Data<Student> {
@Override
public void add(Student stu) {
System.out.println("添加学生!");
}

@Override
public void delete(Student stu) {
System.out.println("删除学生!");
}

@Override
public void update(Student stu) {
}

@Override
public Student query(int id) {
return null;
}
}

泛型通配符

通配符:?
?可以用在使用泛型的时候代表一切类型。
E , T , K , V是在定义泛型的时候使用代表一切类型。

泛型的上下限:
? extends Car : 那么?必须是Car或者其子类。(泛型的上限)
? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见)
public class GenericDemo {
public static void main(String[] args) {
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
run(bmws);

ArrayList<BENZ> benzs = new ArrayList<>();
benzs.add(new BENZ());
benzs.add(new BENZ());
benzs.add(new BENZ());
run(benzs);

ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
// run(dogs); // 就进不来了!
}

// 定义一个方法,可以让很多汽车一起进入参加比赛
public static void run(ArrayList<? extends Car> cars) {

}
}

class Car {
}

class BMW extends Car {
}

class BENZ extends Car {
}

class Dog {
}

自定义泛型方法

修饰符 <泛型变量> 返回值类型 方法名称(形参列表){

}

class class1 {
public static <T> String arrToString(T[] nums) {
StringBuilder sb = new StringBuilder();
sb.append("[");
if (nums != null && nums.length > 0) {
for (int i = 0; i < nums.length; i++) {
T ele = nums[i];
sb.append(i == nums.length - 1 ? ele : ele + ", ");
}
}
sb.append("]");
return sb.toString();
}
}

自定义泛型类

修饰符 class 类名<泛型变量>{

}
泛型变量建议使用 E , T , K , V
class MyArrayList<E> {
private ArrayList lists = new ArrayList();

public void add(E e) {
lists.add(e);
}

public void remove(E e) {
lists.remove(e);
}

@Override
public String toString() {
return lists.toString();
}
}

介绍一下泛型擦除

参考答案

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

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

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

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

扩展阅读

从逻辑上来看,List 是List的子类,如果直接把一个List对象赋给一个List对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个 List
对象,编译器仅仅提示“未经检查的转换”。

上述规则叫做泛型转换,可以通过下面代码进一步理解泛型转换:

List list1 = ...;
List<String> list2 = list1; // 编译时警告“未经检查的转换”

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

参考答案

  • ? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
  • List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;
  • List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。

扩展阅读

在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量,此时如果试图把一个Double对象保存到该Number[]
数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把 List 对象赋值给 List 变量。

数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G 不是 G 的子类型。Foo[]自动向上转型为Bar[]
的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

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

参考答案

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

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

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

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

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

补充

java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够 调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为 java 的反射机 制。

Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每 个类都实现了 Member 接口)。这些类型的对象时由 JVM
在运行时创建的,用以表示未知类里对应的成员。

这样你就可以使用 Constructor 创建新的对象,用 get() 和 set() 方法读取和修改与 Field 对象关联的字段,用 invoke() 方法调用与 Method 对象关联的方法。另外,还可以调用 getFields()
getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。这样匿名对象的信息 就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

import java.lang.reflect.Constructor;

public class ReflectTest {
public static void main(String[] args) throws Exception {
Class clazz = null;
clazz = Class.forName("com.jas.reflect.Fruit");
Constructor<Fruit> constructor1 = clazz.getConstructor();
Constructor<Fruit> constructor2 = clazz.getConstructor(String.class);
Fruit fruit1 = constructor1.newInstance();
Fruit fruit2 = constructor2.newInstance("Apple");
}
}

class Fruit {
public Fruit() {
System.out.println("无参构造器 Run...........");
}

public Fruit(String type) {
System.out.println("有参构造器 Run..........." + type);
}
}

运行结果:无参构造器 Run………..有参构造器 Run………..Apple

补充:java获取反射的三种方法

  1. 通过new对象实现反射机制。
  2. 通过路径实现反射机制。
  3. 通过类名实现反射机制。
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}

方式一(通过建立对象)

Student stu=new Student();
Class classobj1=stu.getClass();

方式二(所在通过路径-相对路径)

Class classobj2=Class.forName("fanshe.Student"); 

方式三(通过类名)

Class classobj3=Student.class;

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

参考答案

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

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

说一说Java的四种引用方式

参考答案

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

  • 强引用:这是Java程序中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
  • 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
  • 弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
  • 虚引用:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

补充

  1. 强引用

    只要引用存在,垃圾回收器永远不会回收

    Object obj = new Object();
    User user=new User();

    可直接通过obj取得对应的对象 如 obj.equels(new Object()); 而这样 obj 对象对后面 new Object 的一个强 引用,只有当 obj
    这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。

  2. 软引用

    非必须引用,内存溢出之前进行回收,可以通过以下代码实现

    Object obj = new Object();
    SoftReference<Object> sf = new SoftReference<Object>(obj);
    obj = null;
    sf.get();//有时候会返回null

    这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象 时,则返回null; 软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的
    真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

  3. 弱引用

    第二次垃圾回收时回收,可以通过如下代码实现

    Object obj = new Object();
    WeakReference<Object> wf = new WeakReference<Object>(obj);
    obj = null;
    wf.get();//有时候会返回null
    wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

    弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时, 将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued
    方法返回对象是否被垃圾回收器标记。

    ThreadLocal 中有使用到弱引用

    public class ThreadLocal<T> {
    static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
    }
    }
    //....
    }
    //.....
    }
  4. 虚引用

    垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

    Object obj = new Object();
    PhantomReference<Object> pf = new PhantomReference<Object>(obj);
    obj=null;
    pf.get();//永远返回null
    pf.isEnQueued();//返回是否从内存中已经删除

    虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引 用。虚引用主要用于检测对象是否已经从内存中删除。

补充:内部类

类的五大成分:成员变量,方法,构造器,代码块,内部类。

  1. 静态内部类

有static修饰,类有的成分它都有,属于外部类本身,会加载一次。

所以它的特点与外部类是完全一样的,只是位置在别人里面而已。

class Outter {
public static int age1 = 12;
private double salary;

// 静态内部类:有static修饰,属于外部类本身,只会加载一次
public static class Inner {
private String name;
private int age;
public static String schoolName = "黑马";

public void show() {
System.out.println(name + "-->" + age + "岁~");
System.out.println(age1);
//System.out.println(salary);
}
}
}
Outter.Inner in=new Outter.Inner();
in.setName("张三");
System.out.println(in.getName());
in.show();
  1. 实例内部类。(成员内部类)

无static修饰的内部类,属于外部类的每个对象的,跟着对象一起加载的。

实例内部类中不能定义静态成员,其他都可以定义。可以定义常量。

class Outter {
public static int age = 1;
private double salary;

// 实例内部类:无static修饰,属于外部类的对象
public class Inner {
private String name;

public static final String schoolName = "黑马";
// 不能在实例内部类中定义静态成员!!!
// public static String schoolName = "黑马";
// public static void test(){
//
// }

// 实例方法
public void show() {
System.out.println(name + "名称!");
System.out.println(age);
System.out.println(salary);
}
}
}
Outter.Inner in=new Outter().new Inner();
in.show();
  1. 局部内部类(几乎不用,局部内部类没啥用)

定义在方法中,在构造器中,代码块中,for循环中定义的内部类

只能定义实例成员,不能定义静态成员,可以定义常量的。

public class InnerClass {

static {
abstract class A {
}
}

public static void main(String[] args) {
class A {
private String name;

public void test() {
}
}
A a = new A();
a.test();
}

public static void test() {
class Animal {
}
class Cat extends Animal {
}
}
}
  1. 匿名内部类(重点)

简化代码,也是开发中常用的形式。

匿名内部类是一个没有名字的内部类。

匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回。

匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型。

new 类名|抽象类|接口(形参){
方法重写。
}

// 匿名内部类写法
new Thread(new Runnable(){
@Override
public void run(){
for(int i=0;i< 10;i++){
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}).start();

补充:数组在内存中的分配

对于 java 数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目。

静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:

//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4  
String[]computers={"Dell","Lenovo","Apple","Acer"}; //1

//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
String[]names=new String[]{"多啦A梦","大雄","静香"}; //2

动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如:

//只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null  
String[]cars=new String[4]; //3

因为 java 数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所 示:

由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化 方式,程序员虽然没有显示的指定初始化值,但是因为 java 数组是引用类型的变量,所以系统也为每个元素分配 了初始化值 null
,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值 也是对应的默认值0。