一、Java面向对象知识结构
- Java语法和关键字,属于形而下的语言规范,如接口与类、内部类,final/finally/finalize,throw/throws,域访问符权限等;
- Java 面向对象思想及体系,属于形而上的设计思想。
二、典型面试例题及思路分析
2.1 "Java 有没有 goto? 如果有,一般用在什么地方?如果没有,如何跳出当前的多重嵌套循环?"goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。
Java 中跳出多重循环有三种方式:
1、break + 标签。在最外层循环前加一个标签如 label,然后在最里层的循环使用用 break label。
public static void main(String[] args) {
label: //标记
for (int i = 0 ; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i = " + i + ", j = " + j);
if(j == 5) { //满中一定条件跳到某个标记
break label;
}
}
}
}
2、通过捕获异常。
public static void main(String[] args) {
try {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i = " + i + ", j = " + j);
if (j == 5) {// 满足一定条件抛异常
throw new RuntimeException("test exception for j = 5");
}
}
}
} catch (RuntimeException e) { //循环外层捕获异常
e.printStackTrace();
}
}
3、通过标置变量。
public static void main(String[] args) {
boolean flag = false; //初始化标置变量
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i = " + i + ", j = " + j);
if (j == 5) { //满足一定条件进行设置标置变量
flag = true;
}
if (flag) { //内层循环判断标置变量
break;
}
}
if (flag) {//外层循环判断标置变量
break;
}
}
}
James Gosling(Java 之父)编写的《The Java Programming Language》一书的附录中给出了一个 Java 关键字列表,其中有 goto 和 const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉 C 语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字。
Java 中支持带标签的 break 和 continue 语句,作用有点类似于 C 和 C++ 中的 goto 语句,但是正因为如同避免使用 goto 一样,应该避免使用带标签的 break 和 continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用。
所以可以在答案的最后加上一些自己的看法,比如说:goto 会破坏程序的可读性,java 中没有大范围使用 goto 反而是好事,而且还应该避免使用带标签的 break 语句。
三、扩展阅读
扩展题目 : 1)、Java 中的 final 关键字有哪些用法?
(1)修饰类:表示该类不能被继承;
(2)修饰方法:表示方法不能被重写;
(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)
扩展题目 :2)、关键字 switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?
在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
2.2 "抽象类(abstract class)和接口(interface)有什么异同?"
相同点:
(1)不能直接实例化。如果要实例化,抽象类变量必须实现所有抽象方法,接口变量必须实现所有接口未实现的方法。
(2)都可以有实现方法(Java8 以前的接口不能有实现方法)。
(3)都可以不需要实现类或者继承者去实现所有方法(Java8 以前的接口,Java8 及以后的接口中可以包括默认方法,不需要实现者实现)。
不同点:
(1)抽象类和接口所反映出的设计理念不同。抽象类表示的是对象 / 类的抽象,接口表示的是对行为的抽象。
(2)抽象类不可以多重继承,接口可以多重继承。即一个类只能继承一个抽象类,却可以继承多个接口。
(3)访问修饰符 ——
抽象类中的抽象方法可以用 public protected 和 default abstract 修饰符,不能用 private、static、synchronize、native 修饰;变量可以在子类中重新定义,也可以重新赋值;
接口的方法默认修饰符是 public abstract, Java8 开始出现静态方法,多加 static 关键字;变量默认是 public static final 型,且必须给其初值,在实现类中也不能重新定义,也不能改变其值。
(4)抽象类可以有构造器,接口没有构造器。
点评:
这个问题可以有更优雅的问法:你一般在哪种情况下使用抽象类,哪种情况下使用接口。当然意思是差不多的,只不过更多考察在工程实践中的应用。
答案可以说的细节很多,比如说不同点中的第 1 项,还可以再细节一些,说抽象类的继承用 extends 关键字,接口的继承用 implements 关键字。但这并不给你太多的加分。重点是答案中的黑体部分:
要从设计思想和设计本质上回答(不同点中的第 1 点)。这个点上加入自己的理解,或者加一些强调的表述。比如说,“抽象类和接口的差异点很多,比如说子类实现二者的关键字不同(extends 和 implements)、抽象类不可以多重继承而接口可以,但是我觉得二者最重要的差异还是设计思想上的不同,抽象类表示的是一种对象的抽象,接口是 xxxx”。这表示候选人对接口和抽象类是有过思考,有自己的理解的。
接口在 java8 中引入了一个重要变化,就是可以在接口中实现方法(相同点中的 1 和 2),这表示候选人是有跟踪学习比较新的技术,软实力中的主动性和学习能力都不错。
加入自己实战中的使用经验,比如说哪些场景上使用接口,哪些场景使用抽象类。这说明候选人不是纸上谈兵,而是真正在实践中有过使用和考虑,当然你也要知道你项目中使用接口和抽象类中背后的考虑。这些都是加分项。
扩展题目 : 抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized 修饰?为什么?都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的; 本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的; synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的;
2.3:“Java 创建对象的方式有哪些?”
(1)使用 new 关键字;
(2)反射,使用 java.lang.Class 类的 newInstance 方法。
这种方式会调用无参的构造函数来创建对象,有两种实现方式。
//方式一,使用全路径包名
User user = (User)Class.forName("com.imooc.interview.demo.User").newInstance();
//方法二,使用class类
User user = User.class.newInstance();
(3)反射,使用 java.lang.reflect.Constructor 类的 newInstance 方法。
Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();
(4)使用 clone 方法。
public class User implements Cloneable {
/** 构造方法 */
public User(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
private Integer age;
// 重写(Overriding)Object的clone方法
@Override
protected User clone() throws CloneNotSupportedException {
return (User) super.clone();
}
public static void main(String[] args) throws Exception {
User person = new User(new Integer(200));
User clone = person.clone();
System.out.println("person == clone, result = " + (person == clone)); // false,拷贝都是生成新对象
System.out.println("person.age == clone.age, result = " + (person.getAge() == clone.getAge())); // true,浅拷贝的成员变量引用仍然指向原对象的变量引用
}
}
首先需要明确两个概念:浅拷贝和深拷贝。
-
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,对拷贝后对象的引用仍然指向原来的对象。
-
深拷贝:不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。
其他需要注意的是,clone () 是 Object 的 native 方法,但如果要标注某个类是可以调用 clone (),
该类需要实现空接口Cloneable。
(5)使用反序列化。
为了序列化 / 反序列化一个对象,需要该类实现空接口 Serializable。
序列化时首先创建一个输出流对象 oos, 使用 oos 的 writeObject () 方法将 p 对象写入 oos 对象中去。使用反序列化创建对象时,首先创建一个输入流对象 ois,使用输入流对象 ois 的 readObject () 方法将序列化存入的对象读出,重新创建一个对象。
序列化是深拷贝。
点评:
前面三种创建方式比较常见,其中第二三种经常见于框架代码,用于 bean 注入等。第四五种本质是复制,是不需要调用构造器的(见下表),第五种在 rpc 调用中使用比较多,第四种使用比较少一些。
创建对象方式 是否调用了构造器
new 关键字 是
Class.newInstance 是
Constructor.newInstance 是
Clone 否
反序列化 否
问:java.lang.Class 类的 newInstance 方法和 java.lang.reflect.Constructor 类的 newInstance 方法有什么区别?
• Class类的newInstance只能触发无参数的构造方法创建对象,而构造器类的newInstance能触发有参数或者任意参数的构造方法来创建对象。
• Class类的newInstance需要其构造方法是public的或者对调用方法可见的,而构造器类的newInstance可以在特定环境下调用私有构造方法来创建对象。
• Class类的newInstance抛出类构造函数的异常,而构造器类的newInstance包装了一个InvocationTargetException异常。
说明:Class类本质上调用了反射包Constructor中无参数的newInstance方法,捕获了InvocationTargetException,将构造器本身的异常抛出
问:面向对象的特征有哪些?
-
抽象:
抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。 -
继承:
继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 -
封装:
封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。 -
多态性:
多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
问:代码中如何实现多态?
实现多态主要有以下三种方式:
接口实现
继承父类重写方法
同一类中进行方法重载
问:overload和overide的区别?
Override 可以翻译为覆盖,从字面就可以知道,它是覆盖了一个方法并且对其重写,以求达到不同的作用。对我们来说最熟悉的覆盖就是对接口方法的实现,在接口中一般只是 对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。在覆盖要注意 以下的几点:
覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
覆盖的方法的返回值必须和被覆盖的方法的返回值一致;
覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
被覆盖的方法不能为 private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
Overload 可以翻译为重载,它是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,VM就会根据不同的参数样式,来选择合适的方法执行。在使用重载要注意以下的几点: 5. 在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是 fun(int,float),但是不能为 fun(int,int)); 5. 不能通过访问权限、返回类型、抛出的异常进行重载; 5. 方法的异常类型和数目不会对重载造成影响; 5. 对于继承来说,如果某一方法在父类中是访问权限是 private,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。
总之,重写(Override) 是父类与子类之间多态性的一种表现,重载 (Overload) 是一个类中多态性的一种表现。很重要的一点就是,Overloaded 的方法是可以改变返回值的类型。
另外:在Java语言规范中,一个方法的特征仅包括方法的名字,参数的数目和种类,而不包括方法的返回类型,参数的名字以及所抛出来的异常。在Java编译 器检查方法的重载时,会根据这些条件判断两个方法是否是重载方法。但在Java编译器检查方法的置换时,则会进一步检查两个方法(分处超类型和子类型)的返还类型和抛出的异常是否相同。
问:使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
使用 final 关键字修饰一个变量时,是指引用变量(地址)不能变,引用变量所指向的对象中的内容还是可以改变的。例如,对于如下语句:
final StringBuffer a = new StringBuffer("immutable");
执行如下语句将报告编译期错误:
a=new StringBuffer("im");
但是,执行如下语句则可以通过编译:
a.append(" broken!");
有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:
public void method(final StringBuffer param){
}
实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象:
param.append("a");
问:静态变量和实例变量的区别?
在语法定义上的区别:静态变量前要加 static 关键字,而实例变量前则不加。
在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
例如,对于下面的程序,无论创建多少个实例对象,永远都只分配了一个 staticVar 变量,并且每创建一个实例对象,这个 staticVar就会加1;但是,每创建一个实例对象,就会分配一个 instanceVar,即可能分配多个 instanceVar,并且每个 instanceVar 的值都只自加了 1 次。
public class VariantTest{
public static int staticVar = 0;
public int instanceVar = 0;
public VariantTest(){
staticVar++;
instanceVar++;
System.out.println("staticVar=" + staticVar + ",instanceVar=" + instanceVar);
}
public static void main(String[] args) {
VariantTest v1 = new VariantTest();
VariantTest v2 = new VariantTest();
VariantTest v3 = new VariantTest();
VariantTest v4 = new VariantTest();
VariantTest v5 = new VariantTest();
}
}
staticVar=1,instanceVar=1
staticVar=2,instanceVar=1
staticVar=3,instanceVar=1
staticVar=4,instanceVar=1
staticVar=5,instanceVar=1
问:&和&&的区别?
&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
问:父类的静态方法能否被子类重写?
不能。重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏,调用的方法为定义的类所有的静态方法。
问:什么是不可变对象?
不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。
问:能否创建一个包含可变对象的不可变对象?
可以,不过需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。
最常见的例子就是对象中包含一个String对象的引用.