Java面试题(21-40)

21、面向对象的特征有哪些方面?

计算机软件系统是现实生活中的业务在计算机中的映射,而现实生活中的业务其实就是一个个对象协作的过程。面向对象编程就是按现实业务一样的方式将程序代码按一个个对象进行组织和编写,让计算机系统能够识别和理解用对象方式组织和编写的程序代码,这样就可以把现实生活中的业务对象映射到计算机系统中。

面向对象的编程语言有抽象、封装、继承 、多态等 4 个主要的特征。

抽象

在面向对象编程中,抽象所指的就是对于要处理的客观对象,在软件系统中也构造相应的对象,提取其中需要关注的属性,而摒弃那些无关的属性,提炼对象的行为。

封装

所谓封装,指的是对象的实现方案的隐藏。其一是对对象属性的隐藏、其二是对对象行为的实现方案的隐藏。

要理解为什么要封装,首先我们必须要知道,从根本上说,大致有两方面的人会接触到面向对象编程:类的创建者和类的使用者。对类的使用者来说最主要的目标是知道某个类的某个方法能解决什么问题,然后通过调用这个类的具体方法快速解决需要解决的问题。而对类的创建者来说,他们的目标是从头到尾构建一个类,只向类的使用者提供一些接口,其他所有的实现细节都隐藏起来。为什么要这么做呢?主要有以下原因:

  1. 隐藏之后,类的使用者就不能接触和改变类的实现细节,所以原创者就不用担心自己的作品会收到非法篡改,可确保他们不会对其他人造成影响。

  2. 隐藏之后,允许类创建者修改内部结构而不影响类使用者的使用。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。如果类的实现方案对类的使用者隐藏,类开发者对类进行优化之后,不会对类使用者造成什么影响。

继承

想象这么一个场景:我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非常令人灰心的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。

但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。在 Java 语言中,继承是通过 extends 关键字实现的。

继承划分了类的层次性,父类代表的是更一般、更泛化的类,而子类则是更为具体、更为细化;

继承是实现代码重用、扩展软件功能的重要手段,子类中与父类完全相同的属性和方法不必重写,只需写出新增或改写的内容,这就是说子类可以复用父类的内容,不必一切从零开始。

java只支持单一继承,多重继承要利用接口来实现。

多态

所谓多态,其实所指的是同一属性或者方法的不同表现形式。对于面向对象而言,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编译之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。

Java实现多态有三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类。

  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备既能调用父类的方法和子类的方法。

在Java中有两种形式可以实现多态:继承和接口。

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。

如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。

在接口的多态中,指向接口的引用必须是实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。

22、java 中实现多态的机制是什么?

靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

23、abstract class 和 interface 有什么区别?

  • 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。

  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。

  • 构造方法,类方法(用static修饰的方法)不能声明为抽象方法。

  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final。

下面比较一下两者的语法区别:

  • 抽象类可以有构造方法,接口中不能有构造方法。

  • 抽象类中可以有普通成员变量,接口中没有普通成员变量

  • 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

  • 抽象类中的抽象方法的访问类型可以是 public,protected;但接口中的抽象方法只能是 public 类型的,并且默认即为 public abstract 类型。

  • 抽象类中可以包含普通方法,接口中所有的方法都是抽象的。

  • 抽象类中可以包含静态方法,接口中不能包含静态方法;

  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是 public static final 类型,并且默认即为 public static final 类型。

  • 一个类可以实现多个接口,但只能继承一个抽象类。

  • 接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用;

24、abstract 的 method 是否可同时是 static,是否可同时是native,是否可同时是 synchronized?

  • abstract 的 method 不可以是 static 的,因为抽象的方法是要被子类实现的,而 static 与子类扯不上关系;

  • native 方法表示该方法要用另外一种依赖平台的编程语言实现的,不存在着被子类实现的问题,所以,它也不能是抽象的,不能与 abstract 混用。例如,FileOutputSteam 类要和硬件打交道,底层的实现用的是操作系统相关的 api 实现,

  • synchronized用于防止多个线程同时调用一个对象的该方法,与static连用可防止多个线程同时调用一个类的该方法,用synchronized的前提是该方法可以被直接调用,所以abstract与synchronized不能同时使用;

25、什么是内部类?Static Nested Class 和 Inner Class 的不同?

内部类就是在一个类的内部定义的类,内部类中不能定义静态成员,内部类可以直接访问外部类中的成员变量,内部类可以定义在外部类的方法外面,也可以定义在外部类的方法体中,如下所示:

public class Outer {
    int out_x = 0;
    public void method(){
        Inner1 inner1 = new Inner1();
        class Inner2{
            public void method(){
                out_x = 3;
            }
        }
        Inner2 inner2 = new Inner2();
    }
    public class Inner1{

    }
}

在方法体外面定义的内部类的访问类型可以是 public,protected,默认,private 等 4 种类型,这就好像类中定义的成员变量的访问控制类型一样,他们决定这个内部类的定义对其他类是否可见;对于这种情况,我们也可以在外面创建内部类的对象,创建内部类的实例对象时,一定要先创建外部类的实例对象,然后用这个外部类的实例对象去创建内部类的实例对象,例如:

Outer outer = new Outer();
Outer.Inner1 inner1 = outer.new Inner1();

在方法内部定义内部类前面不能有访问修饰符,因为这好像是方法中定义的局部变量一样,但是这种内部类的前面可以是final或abstract修饰符。这种内部类对其他类是不可见的,其他类无法引用这个内部类,但是这种内部类创建的实例对象可以传递给其他类访问。这种内部类必须是先定义,后使用,即内部类的定义代码必须出现在使用该类之前,这与方法中的局部变量必须先定义后使用的道理也是一样的。这种内部类可以访问方法体中的局部变量,但是,该局部变量前必须加 final 修饰符。

在方法体内部还可以采用如下语法来创建一种匿名内部类,即定义某一接口或类的子类的同时,还创建了该子类的实例对象,无需为该子类定义名称:

new Thread(new Runnable() {
    @Override
    public void run() {
        //... ...
    }
}).start();

最后,在方法外部定义的内部类前面可以加上 static 关键字,从而成为 Static Nested Class,它不再具有内部类的特性,所以,从狭义上讲,它不是内部类。Static Nested Class 与普通类在运行时的行为和功能上没有什么区别,只是在编程引用时的语法上有一些差别,它可以定义成 public、protected、默认的、private等多种类型,而普通类只能定义成 public 和默认的这两种类型。在外面引用 Static Nested Class 类的名称为“外部类名.内部类名”。在外面不需要创建外部类的实例对象,就可以直接创建 Static Nested Class,例如,假设 Inner 是定义在 Outer 类中的 Static Nested Class,那么可以使用如下语句创建 Inner 类:

Outer.Inner1 inner1 = new Outer.Inner1();

由于 static Nested Class 不依赖于外部类的实例对象,所以,static Nested Class不能访问外部类的非 static 成员变量。当在外部类中访问 Static Nested Class 时,可以直接使用 Static Nested Class 的名字,而不需要加上外部类的名字了,在 Static Nested Class 中也可以直接引用外部类的 static 的成员变量,不需要加上外部类的名字。

在静态方法中定义的内部类也是 Static Nested Class,这时候不能在类前面加 static 关键字,静态方法中的 Static Nested Class 与普通方法中的内部类的应用方式很相似,它除了可以直接访问外部类中的 static 的成员变量,还可以访问静态方法中的局部变量,但是,该局部变量前必须加 final 修饰符。

26、内部类可以引用它的包含类的成员吗?有没有什么限制?

完全可以。如果不是静态内部类,那没有什么限制!

如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员,例如,下面的代码:

static int x=0;
public static class Inner1 {
public Inner1() {
        x=1;
    }
}

27、Anonymous Inner Class ( 匿名内部类 ) 是否可以extends( 继承 ) 其它类,是否可以implements( 实现)interface(接口)?

可以继承其他类或实现其他接口。不仅是可以,而是必须!

继承其他类实例:

public abstract class Person {
    public abstract void eat();
}

public class PersonDemo {
    public static void main(String[] args) {
        Person person = new Person() {
            @Override
            public void eat() {
                System.out.println("匿名内部类继承实例");
            }
        };
        person.eat();
    }
}

实现接口实例:

new Thread(new Runnable() {
    @Override
    public void run() {}
}).start();

28、super.getClass()方法调用

下面程序的输出结果是多少?

import java.util.Date;
public class Test extends Date{
    public static void main(String args[]){
          new Test().test();
    }

    public void test(){
        System.out.println(super.getClass().getName());
    }
}

getClass方法来自Object类,它返回对象在运行时的类型,因为在运行时的对象类型是Test,所以this.getClass()和super.getClass都是返回Test。

由于getClass()在Object类中定义成了final,子类不能覆盖该方法,所以,在test方法中调用getClass().getName()方法,其实就是在调用从父类继承的getClass()方法,等效于调用super.getClass().getName()方法,所以,super.getClass().getName()方法返回的也应该是Test。

如果想得到父类的名称,应该用如下代码:getClass().getSuperClass().getName();

29、String 是最基本的数据类型吗?

基本数据类型包括 byte、int、char、long、float、double、boolean 和 short。

30、String s = “Hello”;s = s + ” world!”;这两行代码执行后,原始的 String 对象中的内容到底变了没有?

没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s 原先指向一个 String 对象,内容是 “Hello”,然后我们对 s 进行了+操作,那么 s 所指向的那个对象是否发生了改变呢?答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为”Hello world!”,原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。

通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,

或者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer 类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。

同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化,把它设置为初始值,应当这样做:

public class Demo {
    private String s;
    //... ...
    public Demo() {
        s = "Initial Value";
    }
    //... ...
}

而非

s = new String("Initial Value");

后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的 String 类型属性 s 都指向同一个对象。

上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java 认为它们代表同一个String 对象。而用关键字 new 调用构造器,总是会创建一个新的对象,无论内容是否相同。

至于为什么要把 String 类设计成不可变类,是它的用途决定的。其实不只 String,很多 Java 标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以 Java 标准类库还提供了一个可变版本,即 StringBuffer。

31、是否可以继承 String 类?

String 类是 final 类故不可以继承。

32、String s = new String(“xyz”);创建了几个 String Object?

两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。new String() 每写一遍,就创建一个新的对象,它依据那个常量”xyz”对象的内容来创建出一个新 String 对象。如果以前就用过”xyz”,这句代表就不会创建”xyz”自己了,直接从缓冲区拿。

33、String、StringBuffer、StringBuilder区别

是否可变

  • String:底层利用字符数组保存字符串常量,是不可变的,因为String类的原码中有:private final char value[];因为有final修饰,所以String类的对象是不可改变的。所以每次修String对象的值时,实际上是生成了一个新的对象,而引用指向了新的String对象;

  • StringBuffer和StringBudder底层是利用字符数组保存字符串变量的,在jdk1.7中它们都继承了AbstractStringBuilder类,而在AbstractStringBuilder类中有char[] value;,所以这两者对象是可变的;

执行速度

一般情况StringBuilder > StringBuffer > String

  • String:因为String对象是不可变的,所以每次修String对象的时候,实际上是生成了一个新的对象,而引用指向了新的String对象;所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。

  • StringBuffer:而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。

是否线程安全

  • String:String中的对象是不可变的,也就可以理解为常量,显然线程安全。

  • StringBuffer:是线程安全的,因为对方法加了同步锁或者对调用的方法加了同步锁。

  • StringBuilder :并没有对方法进行加同步锁,所以是非线程安全的。

接着要举一个具体的例子来说明,我们要把 1 到 100 的所有数字拼起来,组成一个串。

StringBuffer sbf = new StringBuffer();
for(int i=0;i<100;i++){
    sbf.append(i);
}

上面的代码效率很高,因为只创建了一个 StringBuffer 对象,而下面的代码效率很低,因为创建了 101 个对象。

String str = new String();
for(int i=0;i<100;i++){
    str = str + i;
}

34、数组有没有 length()这个方法? String 有没有 length()这个方法?

数组没有 length()这个方法,有 length 的属性;

String 有有 length()这个方法;

35、下面这条语句一共创建了多少个对象: String s=”a”+”b”+”c”+”d”;

答:对于如下代码:

String s1 = "a";
String s2 = s1 + "b";
String s3 = "a" + "b";

System.out.println(s2 == "ab");
System.out.println(s3 == "ab");

第一条语句打印的结果为 false,第二条语句打印的结果为 true,这说明 javac 编译可以对字符串常量直接相加的表达式进行优化,不必要等到运行期去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。

题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个 String 对象。写如下两行代码,

String s = "a" + "b" + "c" + "d";
System.out.println(s == "abcd");

最终打印的结果应该为 true。

36、try {}里有一个 return 语句,那么紧跟在这个 try 后的finally{}里的code会不会被执行,什么时候被执行,在return前还是后?

return语句并不一定就结束一段程序,当它和finally一起使用但是finally语句中无return时会先等finally语句执行完成后再返回值,当finally语句中有return语句时会直接返回finally中return的语句。

37、final, finally, finalize 的区别

final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。

  • 如果一个类被声明为final,就意味着它不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract抽象类的和final的类。

  • 如果将变量或者方法声明为final,可以保证它们在使用中不被改变。

  • 被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。
    被声明final的方法只能使用,不能重载。

finally是异常处理语句结构的一部分,表示总是执行。

  • finally是对Java异常处理模型的最佳补充。finally结构使代码总会执行,而不管有无异常发生。使用finally可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,如果程序员把数据库连接的close()方法放到finally中,就会大大降低程序出错的几率。

finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。

  • Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在Object类中定义的,因此所的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

https://www.cnblogs.com/smart-hwt/p/8257330.html

38、运行时异常与一般异常有何异同?

异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java 编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。

39、error 和 exception 有什么区别?

Error类和Exception类的父类都是throwable类,他们的区别是:

  • Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

  • Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

https://blog.csdn.net/lifengguo_njupt/article/details/7990485
https://blog.csdn.net/zyhlwzy/article/details/80711425

40、Java 中的异常处理机制的简单原理和应用

异常是指 java 程序运行时(非编译)所发生的非正常情况或错误,与现实生活中的事件很相似,现实生活中的事件可以包含事件发生的时间、地点、人物、情节等信息,可以用一个对象来表示,Java 使用面向对象的方式来处理异常,它把程序中发生的每个异常也都分别封装到一个对象来表示的,该对象中包含有异常的信息。

Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error 和 Exception,

Error 表示应用程序本身无法克服和恢复的一种严重问题,程序只有死的份了,例如说内存溢出和线程死锁等系统问题。

Exception 表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常;

系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException ),空指针异常(NullPointerException)、类转换异常(ClassCastException);

普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。

java 为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须 try..catch 处理或用 throws 声明继续抛给上层调用方法处理,所以普通异常也称为 checked 异常,而系统异常可以处理也可以不处理,所以,编译器不强制用 try..catch 处理或用 throws 声明,所以系统异常也称为 unchecked 异常。

猜你喜欢

转载自blog.csdn.net/zyhlwzy/article/details/81270144