[Java程序员面试笔记] 面试笔试部分 -- java 基础

1 Java 的跨平台机制

面试题1 Java语言的优势

  • 请简述Java语言相比其他高级语言有哪些优势

从语言特性的角度:Java 是纯粹的面向对象语言,更能直观的反映现实世界
从平台无关性的角度:Java 语言可以“一次编译,到处运行”,只要安装了特定平台解释器的系统都可以解释执行Java 程序
从开发的角度:Java 提供了很多功能丰富的内置类库,更利于软件开发
从安全性的角度:Java 语言具有更高的安全性和健壮性

面试题2 简述java和c++ 的相同点和不同点

  • 相同点
    二者都是面向对象的语言,都使用面向对象的程序设计思想进行编程,都具有面向对象的基本特征(继承、封装、多态)
  • 不同点
    总结起来,java 和c++ 存在4大不同

第一,java语言是纯粹的面向对象语言,而c++不是
java的所有对象实现必须在类中,所有的方法一定是在类中定义的方法,所有的变量或者对象也必须定义在类中。
因此,java中不存在全局变量或者全局函数
而c++为了兼容c语言面向过程的程序设计特性,c++允许在类外定义main函数并定义全局变量或者全局函数。

第二,java是解释型语言, 具有平台无关性,而c++是编译型语言,是平台相关的

对于java程序,编译器首先将源代码编译成字节码(class文件),然后由java虚拟机(JVM)解释执行。
对于c++程序,源代码经过编译、链接后即可生成可执行文件(exe文件),即机器可以识别的二进制代码。

所以java语言的执行效率不如c++语言(因为需要JVM解释执行),但java 语言具有更好的可移植性

第三,java和c++在很多技术细节上存在差异
1.java 没有指针的概念,避免了c++中操作指针可能引起的系统问题(无效的指针引用等)
2.java不支持多重继承,但可以实现多个接口,从而有效避免了由于多重继承引发的二义性
3.java 不需要手动释放堆上分配的内存。因为java语言提供了垃圾回收机制,所以不需要程序显式的管理内存的释放。
4. c++支持预处理,而java不支持预处理,因此java 是纯粹的面向对象语言,而c++ 带有c的影子
5. c++支持运算符重载,而java 不支持运算符重载
6. c++支持自动强制类型转换,也就是说,c++中可将数值范围大的类型的数据通过赋值语句自动转换为表示数值范围小的类型,从而产生摄入误差。但是java要想进行转换,必须进行显式的强制类型转换。这样可以提高程序的安全性。
7. c++依然支持goto语言,java 不支持goto语句,但是在java中goto仍是保留字

第四,java提供了一些功能强大的标准库(例如用于数据库访问的JDBC库,用于实现分布式对象的RMI库等),这样可以缩短项目开发周期,提高开发的效率。因此,java在一些大型系统的开发中更具有优势。

2 java 的数据类型

面试题1 简述不同的数据类型之间的转换规则

Java的数据类型转换可以分为两类:自动类型转换和强制类型转换

  • (1) 自动类型转换
    它是将低级数据类型自动转为高级数据类型的一种方式、
    当进行自动类型转换的时候,有以下几点需要注意:
    一,char类型的数据转为高级类型(int, long等)时, 会将其自动转换为ASCII码。
    二, 基本数据类型与boolean类型之间不能互相转换。
    三,任何基本类型的值和字符串进行连接运算时,基本数据类型的值都会转为字符串进行运算。
    四,当使用拓展运算符例如“+=”时不会产生自动类型转换
  • (2) 强制类型转换
    它是将高级数据类型转为低级数据类型的一种方式。
    需要注意的是,由于强制类型转换是把范围较大的类型数值赋值给范围较小的类型变量,所以可能会损失精度。

面试题2 判断下面的赋值语句是否正确

short s1 = 1; s1 = s1 + 1;有错误吗? short s1 = 1;s1+=1; 有错误吗?

第一句short s1 = 1; s1 = s1 + 1;有错误。s1+1 的运算结果会被提升到int类型,因此编译器将会报告需要强制类型转换的错误。
第二句 short s1 = 1;s1+=1; 没有错误,可以正常编译运行,因为使用拓展运符“+=”时隐含的进行了强制类型转换。

面试题3 char型字符变量中能否存储一个汉字,为什么?

在java中,char类型的变量占用两个字节大小的空间,因此char类型的数值范围是0-65535.
同时,在java中使用16位的Unicode 编码集作为编码方式,Unicode编码覆盖了世界上所有的具有书面语言的字符,因此java是支持各种语言字符的。
所以char类型变量中可以存储一个中文汉字。

3 运算符

面试题1 说一说&和&&的区别

&和&&都是逻辑运算度,它们的区别在于“&” 是不短路与,“&&”是短路与。
另外,还有一个陷阱,在说区别时,还应说明“&”还表示按位与操作,这样的表述才完整。

面试题2 用最有效率的方法算出2乘以8等于几

本题最简单的解法当然是用2直接乘以8,即用乘法运算符“*”计算。
但是这样做不符合题目中要求的“用最有效率的方法”的要求。
本题最有效率的解法是使用位运算, 我们知道将一个数左移一位表示将该数乘以2,所以对于本题只需要将2左移3位即能实现2乘以8的运算。
由于位运算是直接作用于数据位上的操作,所以相比于乘法运算要高效的多
综上,答案int a = 2<<<3;

面试题3 简述“==” 和“equals”有什么区别

**比较运算符“==”**的作用是判断两个值是否相等,如果相等则返回true,如果不相等则返回false。
更加深入一点的说, “ = =” 是用来判断两个变量的值是否相等,也就是比较变量内存中存储的数值是否相等。
有两种形式的比较需要用到比较运算符“ = =”
一是两个基本数据类型变量之间的比较,二是两个引用类型变量之间的比较
对于基本数据类型,它就是比较两个变量的值是否相等
对于引用类型,它其实是在比较两个引用变量是否引用的是同一块堆内存空间,或者说是否指向同一块堆内存空间。

equals方法对于字符串类型来说,是用来比较两个字符串的内容是否相等,
java 的很多类中也定义了equals方法,这个方法用来比较两个独立对象的内容是否相同,而不是比较引用值本身的。

也就是说,如果一个自定义的类中没有显式地定义equals方法,那么equals方法的作用与比较运算符“==” 是一样的,都是用来比较两个标量指向的对象是否是同一对象。

  • 答案是:
    运算符“==” 只是用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同。
    equals是类的成员方法,一般它是用来比较两个独立对象的内容是否相等,
    如果自定义的类中没有定义equals方法,则它将继承object类的equals方法,其作用与“= =” 相同。

4 分支语句和循环语句

面试题1 简述java 中为什么没有goto语句

goto是Java的关键字,所以不能用goto 来命名变量
在java中并不能使用goto语句,这是因为虽然goto在java中国作为关键字存在,但并没有实现它。
与goto类似的还有关键字const,这两个关键字在java语言中并没有具体含义。

  • 答案:

goto虽然是Java 的关键字,但是不能被使用,因为goto会导致程序流程的混乱,影响程序的可读性和可维护性,所以在Java中已被废除。可以通过break和continue实现程序在多层循环中的跳转。

面试题2 简述在Java中如何跳出多重循环

在Java中如果想跳出一个循环,一般使用break语句或者continue语句。
break 语句是结束整个循环体,而continue 语句是结束本次循环。
采用加标签的break语句或者加标签的continue语句可以跳出多重循环,而单纯的break语句和continue语句只能跳出本次循环(一充循环)。

  • 答案:
    使用break label, continue label 可以跳出多重循环,还可以通过让外层的循环条件表达式的结果受内层循环体代码控制的方法跳出多重循环,或者在循环体中使用return语句。

5 数组

面试题1 简述java 数组的初始化方法

  • (1) 一维数组
    在Java中可以通过两种方式定义一个一维数组:
type[]  arrayName;
type arrayName[];

需要注意一点,在定义一个一维数组时, 方括号[] 中不能填写任何数字
推荐使用第一种方法定义数组。 定义了一个数组还不能使用它,因为它只是一个引用,还需要指向一个堆内存中的数组实例才能使用,也就是数组的初始化。Java 中初始化数组有两种方法:静态化初始数据和动态化初始数组。

静态化初始数组 是指在定义数组时显式的指定每个数组元素的初始值,系统会根据初始值的个数和类型决定数组的大小。
例如
int[] arrayName; //定义一个数组 arrayName = new int[] {1, 2, 3, 4, 5, 6} //数组的初始化
或者将数组定义和数组的初始化同时完成:

int[] arrayName = {
     1, 2, 3, 4, 5, 6}               //定义数组和数组的初始化同时完成

这样系统会在堆内存中分配6个int类型长度大小的内存空间,并初始化数组元素为1, 2, 3, 4, 5, 6。

除了静态初始化数组外,使用更多的还是动态初始化数组。
动态初始化数组是指仅仅指定数组的长度,不需要指定数组元素的初始值。
例如:

int[]  arrayName;                      //定义一个数组
arrayName = new int[6];                //动态初始化数组,指定数组的长度

或者将数组定义和数组的初始化同时完成:

int[] arrayName = new int[6]               //定义数组和初始化数组同时完成

系统会在堆内存上为数组arrayName 分配6 个int类型长度大小的内存空间

  • (2) 二维数组
    Java 不仅支持一维数组,还支持二维数组。
    二维数组有三种定义方法:
type arrayName[][];
type[][] arrayName;
type[]  arrayName[];

需要注意一点,在定义一个二维数组时, 方括号[] 中不能填写任何数字
与一维数组类似,单纯定义了一个二维数组并不能使用它,还需要指向一个堆内存中的数组实例才能使用。Java 中初始化数组有两种方法:静态化初始数据和动态化初始数组。

静态初始化二维数组 是在定义二维数组时显式的指定每个数组元素的初始值, 系统会根据初始值的个数和类型决定数组的大小。
例如:

int[][] arrayName;                                                           //定义一个数组
arrayName = new int[][]{
     1, 2, 3, 4, 5, 6},{
     7,8,9,10,11};                     //数组的初始化

或者将数组定义和数组的初始化同时完成:

int[][] arrayName = new int[][]{
     1, 2, 3, 4, 5, 6},{
     7,8,9,10,11}; 

动态初始化数组是指仅仅指定数组的长度,不需要指定数组元素的初始值。
例如:

int[][] arrayName;                                                           //定义一个数组
arrayName = new int[6][3];                                                  //动态初始化数组,指定数组的行数和列数

或者将数组定义和数组的初始化同时完成:

int[][] arrayName = new int[6][3]

有一点需要注意,在动态初始化一维或者二维数组时,由于不指定数组元素的初始值,所以系统将负责为这些元素分配初始值。

  • 分配数组元素初始值的规则如下:
    1)byte, short, int, long元素初始值为0
    2)float,double元素初始值为0.0
    3)char元素初始值为’\u0000’
    4)boolean 元素初始值为false
    5)引用类型(各种自定义类型、数组等) 初始化为null

还有一点需要注意,与c++不同,java的二维数组第二个维度的长度可以不同,因此更加灵活。
例如:

int[][] arrayName = {
     {
     1, 2}, {
     3, 4, 5}};

或者

int[][] arrayName = new int[2][];
a[0] = new int[][1, 2];
a[1] = new int[][3, 4, 5];

也就是说,Java 中的 数组每行的列数可以不同,例如上面定义的这个二维数组,第一行中包含两列,而第二行中包含三列。

面试题2 数组有没有length() 这个方法?String 有没有length()方法?

数组没有length() 这个方法,但是有length 属性,可以通过length属性获取到一个数组的长度(无论该数组是否已被赋值)
String 类中没有length的属性, 但是有 length()这个方法,其目的也是获取字符串的长度

6 字符串

理解两个概念,一个是在堆内存上创建的字符串对象,一个是常量池中的字符串常量其区别

  • 首先来看在堆内存上创建一个字符串常量
    例如:
String str = new String ("abc");

其中变量str是一个引用类型的变量,它指向堆内存中的一块空间,里面存放了字符串“abc”。
由于它是一个String类型的对象,所以该字符串的内容不能被修改

而如果是定义以下一个字符串,则情况就不同了,例如:

String str = "abc";

字符串“abc” 是一个字符串常量,它在编译时就被创建,并被保存在.class文件的常量池中。
而在程序运行时,类会被加载到内存中,此时, .class 文件的常量池的内容将在类加载后进入方法区的运行时常量池中。
所以此时引用变量str指向的并不是一般意义上的堆内存中的字符串对象,而是运行时常量池中的字符串常量。

与此同时,Java 会确保每个字符串常量在常量池中只有一个,不会产生多个副本

面试题1 String类型的特性

如果执行了下面这两句程序:

String s = "Hello";
s = s + " world!";

原始的String 对象中的内容到底变了没有?

  • 分析:
    String对象一旦被创建,对象中的字符队列就不能被修改。
    也就是说,在执行完String s = "Hello"; 这条语句后,变量s就指向了字符串常量“Hello”,当字符串s进行了“+” 操作后,引用变量s就不再指向原来的那个字符串常量“Hello”了,而是指向了一个新的字符串常量“Hello World!”, 而原来的那个字符串常量仍然在内容中,只是没有引用变量再指向它了。
    所以原始的String对象中的内容并没有发生任何变化,s指向了一个新的字符串变量“Hello World!”。

从上面的分析,我们能够知道String 是一个不可变类, 当一个字符串需要经常被修改时, 要尽量避免使用String 类存储字符串,而是用StringBuffer 或 StringBuilder 类来实现。因为在使用String类修改字符串时(就像题目中这个例子),会产生一些无用的中间对象
(例如第一个字符串“Hello”),这样的无用对象在内存中不断积累增多而来不及被回收,久而久之会影响程序的性能。

  • 答案
    原始的String对象中的内容并没有发生任何变化,仍然是字符串“Hello”, s 指向了一个新的字符串常量“Hello World!”。

面试题2 简述String, String-Buffer, StringBuilder的区别和适用场景

  • String 类型

String 类型是最基本的字符串类型,它是一个不可变类,也就是说,一旦该类的对象被创建,对象中的字符序列就不能被更改,知道该对象被系统回收。
当一个字符串需要经常被修改时,要尽量避免使用String类存储字符串,因为这样会产生一些无用的对象,影响程序的性能

  • StringBuffer类型
    StringBuffer也是常用的字符串操作类型,与String类型不同,StringBuffer对象代表一个字符序列可变的字符串。
    StringBuffer类提供了append, insert, reverse, setCharAt, setLength等方法,通过这些方法可以修改该字符串对象的字符序列。

  • StringBuilder类型
    StringBuilder是JDK1.5 后新增的一个类, StringBuilder与SrtringBuffer类基本类似,两个类的方法也基本相同,不同的是SrtringBuffer是线程安全的,而StringBuilder 则没有线程安全机制,因此StringBuilder性能略高。
    所以如果在单线程下操作大量数据时应优先使用StringBuilder; 如果在多线程下操作字符串,则应考虑使用SrtringBuffer

7 异常处理

知识点梳理

Java中的异常处理机制由关键字try, catch, finally, throw 和 throws组成。
关键字try后面要紧跟一个由大括号{ } 括起来的代码块,这个代码块一般被简称为try块,里面放置的是可能发生异常的代码。

catch 用来捕获异常。 一个try 可以对应多个catch, try块中可能发生不同类型的异常,而每个catch只能捕获一类异常。一个catch后面对应一个异常类型和一个代码块,代码块中放置的是用于处理对应异常的代码。

finally 关键字后面的代码块称为 finally块, 因为异常机制会保证finally块总会被执行,所以它一般用于处理try块中打开的资源,也可以做一些收尾工作。

关键字throws 用于声明一个方法可能抛出异常,关键字throw用于抛出一个实际的异常。

Java 提供了丰富的异常类,这些异常类之间存在继承关系,如下图所示:
[Java程序员面试笔记] 面试笔试部分 -- java 基础_第1张图片

从上图可知,从Throwable 类中派生出两个子类,异常类(Exception)和错误类(Error)。

**错误类Error一般都是JVM 相关的问题,例如系统崩溃、虚拟机错误等。**程序在执行的过程中一旦发生这些错误将会导致程序的彻底停止,因此Error发生是不可恢复的,程序员必须要去catch’这些error而试图处理它。

需要更多的关注Exception类。从图中可知,Java的Exception分为两大类,一类是Runtime异常,即RuntimeException及其子类抛出的异常; 另一类称为Checked异常,例如图中的IoException,SQLException等以及自定义的异常都属于Checked异常。

所谓Checked异常就是Java 认为可以被发现和处理的异常,程序必须显式的处理Checked异常,如果没有处理这些异常,程序则无法编译通过。

在Java中有两种方式处理Checked 异常:
(1) 在try …catch 语句中使用catch块捕获和修补该异常。
(2)使用throws关键字在定义方法时声明抛出该异常。
这里要注意的是,使用throws会将本方法中发生的异常抛给上一级调用者处理。如果上一级调用者也不catch该异常,而是使用throws关键字在定义方法时声明抛出该异常,那么程序可将该异常继续抛给更上一级调用者。如果该异常一直向上抛,最终被main 方法抛给了上一级,则该异常会交给JVM处理,此时程序会被终止运行,并打印出异常跟踪栈信息。

所谓Runtime异常是指程序运行时发生的异常,这类异常是程序员自身的错误导致的,是完全可以避免的,所以Runtime异常无须程序显式的处理,编译的时候不会检查Runtime 是否被捕获或抛向上层。

  • 特别提示:Checked异常和Runtime异常的本质

Runtime异常一定是程序员的错误导致的,比如空指针NullPointerException就是某一个引用为null,而程序又去调用该引用的一个方法或者属性导致异常;再比如算术异常ArithmeticException,当除数为0时会抛出该异常。这些异常的发生本身时因为代码的缺陷造成的,所以理论上程序员可以避免这些异常的发生。

而Checked异常有可能是一些不可预期的原因造成的,是完全无法避免的,比如IO异常IOException就有可能是在读写文件时发生,但是否发生本身时不可预期的。所以Checked异常需要程序显式的处理(主要是做一些异常发生后的善后工作),虽然大多数情况下这个异常不会真的发生。

  • 那么如何区分Checked异常和Runtime异常呢?
    只需要牢记住那些是Runtime异常,其他的异常(包括自定义的异常)就都是Checked异常了。
    Runtime异常包括以下几种:
    (1)NullPointerException: 空指针引用异常
    (2)ClassCastException: 类型强制转换异常
    (3)IllegalArgumentException: 传递非法参数异常
    (4)ArithmeticException: 算术运算异常
    (5)ArrayStoreException: 向数组中存放与声明类型不兼容对象异常
    (6)IndexOutOfBoundsException: 下标越界异常
    (7)NegativeArrayException:创建一个大小为负数的数组错误异常
    (8)NumberFormatException:数字格式异常
    (9)SecurityException:安全异常
    (10) UnsupportedOperationException:不支持的操作异常

面试题 1 finally 块中的代码什么时候会被执行?

如果try块或catch块中有return,而finally块中没有return,则要先执行finally块中的代码,然后执行return语句。
如果finally块中有return语句,那么它将会覆盖掉try块或catch块中的return 语句。
如果在try块之前有异常发生,则finally块有可能得不到执行。
如果直接调用System.exit(0),强制退出,则finally块也得不到执行。

2 面试题2 Java 异常处理中的关键字

Java中的异常处理机制由关键字try,catch, finally,throws和 throw组成,它们在异常处理机制中起着不同的作用。

  • (1) try…catch
    在try块中放置业务实现代码, 如果执行try块里面的代码时发生异常,系统就会生成一个异常对象并提交给Java运行时环境,这个过程称为抛出异常。
    Java运行时环境收到该异常对象后会寻找能够处理该异常对象的catch块。如果找到了能够处理该异常对象的catch块,则系统将该异常对象交给catch’块处理,这个过程叫做捕获异常; 如果找不到处理该异常对象的catch块,则程序运行终止。

例如:

try{
     
	//业务实现代码
}catch (IOException){
     
	//捕获到IO异常并处理
}

上述代码中,如果业务实现代码中发生了IO异常,则该异常将被代码中的catch所捕获,所以程序不会终止。
如果业务实现代码中发生了其他类型的异常(例如SQLException),则该异常就不能被捕获,因此程序可能被终止。(除非该异常被抛给上层,并且在上层被捕获)。

如果程序员希望无论业务实现代码发生何种异常都能被catch所捕获,那么可以使用Exception类型异常作为catch的参数。
例如:

try{
     
	//业务实现代码
}catch (Exception  e){
     
	//捕获到所有异常并处理
}

但是在实际应用中不建议这么做,因为这样可能会无条件的补货所有异常而导致程序不能得到预期的结果,降低了代码的可维护性。

  • (2)finally
    有时候程序会在try块中打开一些资源,例如,打开数据库连接,打开网络连接,打开磁盘文件等。虽然Java中有回收机制,但是它只能回收Java堆内存中的对象,所以这些物理资源必须要显式的回收。

如何显式的回收这些物理资源呢?

如果将回收物理资源的语句放到try块中,一旦程序在try块中发生异常,那么这些语句可能执行不到。
如果将回收物理资源的语句放到catch块中,那么如果没有发生异常,或者发生的异常没能被catch捕获,则相应的代码块仍然不能执行到。
所以为了确保打开的物理资源可以被回收,就需要finally语句。
不管try块中的代码是否出现异常,也不管catch是否捕获了异常,finally 语句都会被执行(除了上面所说的特殊情况),
所以回收物理资源的语句放在finally块中是最合适的。

  • (3) throws
    如果一个方法中的代码可能发生Checked异常,但是该方法并不知道该如何处理该异常,这种情况下就需要用throws将该异常抛给该方法的调用者。
    例如:
public voud creatFile() throws IOException{
     
	FileInputStream fis = newFileInputStream("test.txt");
	......
}

因为函数creatFile()中不能对这个IO异常作出合适的处理,而IO异常属于Checked异常,必须被显式的处理,所以可以在定义方法时声明抛出IOException。
另外,throws也可以声明抛出多个异常,多个异常类之间用逗号分隔。
例如:

public voud creatFile() throws IOException, SQLException{
     
	FileInputStream fis = newFileInputStream("test.txt");
	......
}
  • (4)throw
    当程序的逻辑走到一个不正确的分支时,可以使用throw关键字主动抛出一个异常,以期待进一步的处理。
    使用关键字throw抛出的异常可以是一个Checked异常,也可以是一个Runtime异常。
    如果抛出的是Checked异常,则该throw语句要么放在try块中显式的被catch捕获,要么就要放在一个带throws声明的方法中。
    因为try块中可以通过throw抛出异常。

8 反射机制

1 反射机制的基本概念

在高级语言中,允许改变程序结构或者变量类型的语言称为动态语言,例如perl, python, Ruby等就是动态语言,而像C, C++, Java这类语言在程序编译时就确定了程序的结构和变量的类型,因此不是动态语言。
尽管如此, Java 还是为开发者提供了一个非常有用的与动态相关的机制–反射(Reflection)。
运用反射机制可以在运行时加载和使用编译期间未知的类型。
也就是说, Java程序可以加载在运行时才得知类名的class,并生成其对象实体,或访问其属性,或唤起其成员方法。
通俗点讲,所谓Java的反射机制,就是在Java程序运行时动态的加载并使用在编译期并不知道的类。

Java的反射机制功能十分强大,
首先反射机制可以动态的获取一个类的信息,包括该类的属性和方法,这个功能可应用于对class文件进行反编译。
其次, 反射机制也可以通过类型的名称动态生成对象,并调用对象中的方法。
因为有时候无法再变一阶段得知一个类的信息,而在程序运行时又需要构造出该类的实例,这个时候反射机制就能派上用场。
另外,一些经典的设计模式也可以给予Java饿反射机制实现,例如简单工厂模式。
除此之外,很多框架都用到反射机制,例如Hibernate, Saruts都是用反射机制实现的。

面试题2 简述反射机制的优缺点

  • 分析

反射是Java中的一个十分重要而有用的机制。它给Java提供了运行时获取一个类实例的可能,只要传递一个类的全包名路径,就能通过反射机制获取对应的类实例,并通过该实例调用其方法和属性, 因此反射机制大大提高了系统的灵活性和可拓展性。
反射在一些开源框架中得到了广泛的应用,例如:Spring,Struts,Hibnerate,MyBatics等都广泛的应用了反射机制。
但是事物都有正反两个方面,反射机制也存在着一些缺点。例如,反射机制会对系统的性能造成影响,因为反射机制是一种解释操作,它是在程序运行时才告诉Java虚拟机去加载某些类,而在一般情况下,运行的所有的程序在编译器就已经把类加载了。
另外,**反射机制破坏了类的封装性,**可以通过反射获取这个类的私有方法和属性,从而导致安全性相对降低。

  • 答案
    优点:可以在运行时获取一个类的实例,大大提高了系统的灵活性和可拓展性
    缺点:性能较差,安全性不高,破坏了类的封装性。

9 关键字

知识点梳理

在Java中一共有50个关键字。其实更确切的讲,Java中的有效关键字为48个,而goto和const一般称为保留字(Reserved Word),这两个保留字都没有实际的意义,不能在Java中使用。除此之外,Java还提供了3个直接量(Literal),分别是true, false 和 null。 Java的标识符的命名既不能使用上述提供的50个关键字,也不能使用这3个直接量。
对Java中的关键字分门别类的加以归纳,这样只需要记住几个大类即可,凡是属于这些类的标识符就是关键字,其余的标识符就不是关键字。可以按照如下的方法进行分类:

  • 1)包、类、接口定义相关的
    class, interface, package, extends, implements, import, abstract
    1. 基本数据类型
      int, float, double, char, long, enum, void, byte, short, boolean
    1. java 语句控制符
      if , while, else, for , break, case, continue, default, do, return , switch, synchronized
    1. 类型、变量、方法修饰符
      final, native, private, protected, public, static, volatile, strictly, transient
    1. 异常处理相关的
      try, throw, throws, catch, finally
  • 6) 类引用
    this, super
    1. 运算符
      new, assert,instanceof
  • 8)保留字
    goto, const

面试题1 简述 final, finally 和 finalize的区别

final, finally 和 finalize是三个看上去很相似的关键字,很容易被混淆,但是他们的含义却不尽相同。

  • 1)final
    final关键字可用来修饰类的属性、方法的参数、方法以及类本身。
    final 修饰属性时表示该属性不可变, final修饰方法是表示该方法不可被覆盖,final修饰类时表示该类不可被继承。

当使用final 修饰属性(变量、对象)时,必须对属性(变量、对象)进行初始化。如果final修饰的变量是基本数据类型,则一旦该变量被初始化就不能被赋予新值,例如:

final int a  = 6;
a = 7;                   //错误,a声明为final,因此不能被赋予新值

如果final修饰的是一个引用类型的对象,则这里指的是不可变是指该引用所引用的地址不能改变,并不表示该引用对象的内容本身不能发生改变。例如:

final  String str = "Hello";
str  = str +  "World!";                  //错误,因为str此时要试图指向一个新的字符串

上面这段程序中,字符串str最初指向字符串常量"Hello", 接下来试图执行str = str + “World!”; 将字符串"Hello" 与字符串"World!" 进行拼接,并用str指向形成的新的字符串(由于字符串类型是不可变类型,所以str指向的字符串本身内容不能被修改)。但是由于str开始被声明为final,所以会发生编译错误。

final StringBuffer str = new StringBuffer("Hello");
str.append("World!");                 //没有问题,str指向的对象内容可以发生改变

再如上面这段代码,虽然str也被声明为final,但是它指向的对象是 StringBuffer类型的对象,其内容可以被修改,所以通过append方法修改StringBuffer里面字符串的内容是可以的。

通过上面两个实例就能更加直观的理解当使用final修饰引用变量时,它只能指向初始时指向的那个对象,而指向对象的内容可以被修改。

当使用final修饰方法的参数时则表示该参数在方法内部不能被修改,它有点类似于c++中用const修饰函数的形参。
当使用final修饰方法时, 该方法不能被子类覆盖重写,但是该方法在子类中仍然可以被使用。与此同时,在Java中用final修饰的函数也被称为内联函数。这个内敛函数类似于c++中的内联函数,它不是必须的,而是在编译时告诉编译器这个函数可以作为内联函数编译。至于最终编译器如何处理则由编译器自己决定。内联函数的优势在于当调用该函数时,系统会直接将方法主体插入调用处,从而省去了方法调用的环境,提升了程序的执行效率。
当一个类被final修饰时,该类不能被继承,因此该类的所有方法也就不能被覆盖并重写。需要注意的是,抽象类(abstract clas) 和接口(interface)不能用final修饰, 因为定义的抽象类和接口就是用来被继承和实现的。

  • 2) finally
    finally这个关键字只有在异常处理时才会出现,它通常与try, catch合用,并自带一个语句块。不管try块中的代码是否发生异常,也不管哪一个catch块得到执行, finally块最终总是会被执行到,所以finally块中的代码常被用来执行资源回收,文件流关闭等操作。

-3)finalize
finalize是Object 类的一个方法。**在垃圾回收机制执行的时候会调用被回收对象的finalize方法。**因为finalize是Object类的方法,所以Java中任何类都可以覆盖这个方法,并在该方法中清理该对象所占用的资源。
因为Java中增加了垃圾回收机制,所以可以省去人们手动释放对象内存空间的麻烦,在很大程度上避免了内存泄漏的发生。那为什么还要有finalize方法呢? 这是因为,有时在撤销一个对象时还需要对一些非Java资源进行处理,例如关闭文件句柄等。这就需要在对象被撤销之前保证这些资源被释放。为了处理这种情形,Java提供了这种所谓收尾机制,使用收尾机制可以在一个对象将要被垃圾回收程序释放时调用到该对象的finalize方法,从而清理一些非Java的资源。
在使用finalize时需要特别注意一点,不要认为finalize方法一定会执行。 垃圾回收机制何时调用对象的finalize方法对程序完全是透明的,当系统中资源充足时,垃圾回收机制可能并不会得到执行。因此如果想要保证某一时刻某个类打开的资源一定被清理,就不要把这个操作放在这个类的finalize方法中执行,因为并不能确定该方法什么时候被执行,是否就会被执行。

面试题2 简述static的作用

在Java中static 关键字有4种使用场景,下面分别进行介绍。

  • 1)static 成员变量
    在类中一个成员变量可用static关键字来修饰,这样的成员变量称为static成员变量,或静态成员变量。而没有用static关键字修饰的成员变量称为非静态成员变量。

静态成员变量是属于类的,也就是说,该成员变量并不属于某个对象,即使有多个该类的对象实例,静态成员变量也只有一个。只要静态成员变量所在的类被加载,这个静态成员变量就会被分配内存空间。 因为在引用该静态成员变量时,通常不需要生成该类的对象,而是通过类名直接引用。引用的方法是“类名. 静态变量名”。当然仍然可以通过“对象名. 静态变量名” 的方式引用该静态成员变量。相对应的非静态成员变量则属于对象而非类,只有在内存中构建该类对象时,非静态成员变量才被分配内存空间。

  • 2)static成员方法
    Java 中也支持用static关键字修饰的成员方法,即静态成员方法。与此相对应的没有用static修饰的成员方法称为非静态成员方法。
    与静态成员变量类似,静态成员方法是类方法,它属于类本身而不属于某个对象。因此静态成员方法不需要创建对象就可以被调用,而非静态成员方法则需要通过对象来调用。

**特别需要注意的是, 在静态成员方法中不能使用this, super关键字,也不能调用非静态成员方法,同时不能引用非静态成员变量。**这个道理是显而易见的,因为静态成员方法属于类而不属于某个对象,而this,super都是对象的引用,非静态成员方法和成员变量也都属于对象。所以当某个静态成员方法被调用时,该类的对象可能还没有被创建,那么在静态成员方法中调用对象属性的方法或成员变量显然是不合适的。即使该类的对象已经被创建,也是无法确定它究竟是调用哪个对象的方法,或是哪个对象中的成员变量的。所以在这里特别强调这一点。

  • 3)static代码块
    static代码块又称为静态代码块,或静态初始化器。它是在类中独立于成员函数的代码块。**static代码块不需要程序主动调用,在JVM加载类时系统会执行static代码块,因此在static代码块中可以做一些类成员变量的初始化工作。**如果一个类中有多个static代码块,JVM将会被顺序依次执行。需要注意的是,所有的static代码块只能在JVM加载类时被执行一次。

  • 4)static内部类
    在Java中还支持用static修饰的内部类,称为静态内部类。静态成员内部类的特点主要是它本身是类相关的内部类,所以它可以不依赖外部类实例而被实例化。 静态内部类不能访问其外部类的实例成员(包括普通的成员变量和方法),只能访问外部类的类成员(包括静态成员变量和成员方法)。即使是静态内部类的实例方法(非 静态成员方法)也不能访问其外部类的实例成员。

面试题3 简述volatile的作用

要理解volatile关键字的作用,首先要了解Java的内存机制。Java内存模型规定,每个线程都有自己的工作内存,它不同于计算机的主存,而更像是一种高速缓存。线程中对变量的所有操作都必须先在工作内存中进行,然后同步到计算机的主存中。理论上线程的工作内存中的变量的值应当与主存中该变量的值保持一致,同时工作内存与主存对于程序都是透明的。

工作内存机制的好处在于每个线程都有自己独立的缓存,可以更加方便高效地从工作内存中读取数据。但是事情总有两方面,工作内存机制赋予多线程的程序也存在一些风险。见p129.

volatile 关键字可以用来修饰变量, 它的作用主要有两个:
1)使用volatile 关键字会强制将修改的值立即写入主存
2)当某个线程修改volatile修饰的变量时,会使该变量在任何线程中都暂时无效,迫使它直接从主存中读取该值。

面试题4 简述instanceof的作用

instanced 关键词的作用是判断一个对象是否是某个类(或者接口, 抽象类,父类)的实例。
它的使用方法是result = A instanced B ,其中A为某个对象的引用,B为某个类的类名(或者接口名、抽象类名、父类名)。
其运算结果result为一个boolean型返回值,如果A是B的一个实例,则返回true;如果A不是B的一个实例,或者A为null,则返回false。

请看下面这个实例:

public class instanceofTest implements A{
     
	public static void main(String[]  args){
     
		String str = "This is a string";
		instanceofTest obj = new instanceofTest();
		System.out.println(str instanceof String);
		System.out.println(str instanceof Object);
		System.out.println(obj instanceof A);
	}
}

interface A{
     
}

在这段程序中创建了两个对象,一个是str指向的字符串,另一个是instanceofTest类的一个实例obj。然后通过instanceof 关键字对这两个对象所属的类进行判断。
因为str指向一个字符串,所以str instanceof String的返回值为true;
因为在Java中任何类都是Object类的字类,所以str instanceof Object 的返回值也为true;
因为instanceofTest实现了接口A(虽然A中也没有什么定义),所以obj instanceof A 的返回值也为true。

面试题4 什么是对象的序列化和反序列化

在计算机中数据都是以二进制的形式存储在磁盘中的,同时数据也都是以二进制流的形式在网络中传输的,Java的对象也不例外。可以通过序列化机制将Java对象转换为字节序列,并将这些字节序列保存到磁盘中或通过网络传输。因此序列化机制可以使得对象可以脱离程序的运行而独立存在。

Java的序列化机制可以包括两个内容,一是对象的序列化,即将一个Java对象写入I/O流中;
另一个则是与之相对应的对象的反序列化,即从I/O流中恢复该Java对象,以提供给程序使用。

在Java中如果一个类实现了Serializable接口,则表明该类的对象是可序列化的。
需要注意的是,Serializable接口只是一个标记接口,实现该接口无需实现任何方法,它只是表示实现该接口的类的对象可被序列化。

使用Serializable实现对象序列化只需要两步就可以完成:
1)使用一个节点流(一般是输出流,例如FileOuputStream)对象构建一个处理流ObjectOuputStream对象;
2)调用ObjectOuputStream对象中的writeObject(object)方法将object序列化,并输出到ObjectOuputStream对象指定的流中。

反序列化的过程与序列化的过程正好相反,也是需要两步就可以完成:
1)使用一个节点流(一般是输入流,例如FileInputStream)对象构建一个处理流ObjectInputStream对象;
2)调用ObjectInputStream对象中的readObject方法读取流中的对象,该方法回返回一个Object类型的对象,可将该对象强制类型转换成其真实的类型。

面试题5 简述什么是序列化版本

在进行对象反序列化时需要提供该类的class对象,但是在项目的实际开发中一个类的定义会随便代码的维护和升级而发生改变,因此对应的class文件也会发生改变,这样就会产生一个反序列化的兼容性问题。

要解决反序列化的兼容性问题,可以在定义类时为序列化类提供一个private static final 的 serialVersionUID 属性,该属性值用于表示该类的序列化版本。只要确保当前class中的serialVersionUID 属性与反序列化对象所属类的serialVersionUID 一致, 该对象就可以被正确的反序列化。

你可能感兴趣的:(Java,程序员面试笔记)