Java基础知识面试题系列三:21~30题
21.抽象类(abstract class)与接口(interface)有什么异同
- 一个类中包含抽象方法,这个类就是抽象类。
- 在Java语言中,可以通过把类或者类中的某些方法声明为abstract(abstract只能用来修饰类或者方法,不能用来修饰属性)来表示一个类是抽象类。
- 接口就是指一个方法的集合,接口中的所有方法都没有方法体,在Java语言中通过关键字interface来实现的。
抽象类和接口都是支持抽象类定义的两种机制(注意:此句中的前后两个抽象类的意义不一样,前者表示的是一个实体,后者表示的是一个概念)。
- 只要包含一个抽象方法的类就必须被声明为抽象类
- 抽象类可以声明方法的存在而不去实现它
- 被声明为抽象的方法不能包含方法体。
- 在实现时,必须包含相同的或者更低的访问级别(public->protected->private).
- 抽象类在使用的过程中不能被实例化,但是可以创建一个对象使其指向具体子类的一个实例。
- 抽象类的子类为父类中的所有抽象方法提供具体的实现,否则它们也是抽象类。
- 接口可以看作抽象类的变体。
- 接口中的所有方法都是抽象的,可以通过接口来间接地实现多重继承。
- 接口中的成员变量都是static final类型。
- 由于抽象类可以包含部分方法的实现,因此在一些场合下抽象类比接口存在更多的优势。
接口与抽象类的相同点如下:
- 都不能被实例化。
- 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化。
接口与抽象类的不同点如下:
- Java8之前,接口只有定义,其方法不能在接口中实现,只有实现接口的类才能实现接口中定义的方法,而抽象类可以有定义与实现,即其方法可以在抽象类中被实现。
- 接口需要实现(implements),但抽象类只能被继承(用extends)。一个类可以实现多个接口,但一个类只能继承一个抽象类,因此使用接口可以间接地达到多重继承的目的。
- 接口强调特定功能的实现,其设计理念是has-a关系;而抽象类强调所属关系设计理念为is-a关系。
- 接口定义的成员变量默认为public static final,只能够有静态的不能被修改的数据成员,而且,必须给其赋始值,其所有成员方法都是public、abstract的,而且只能被这两个关键字符修饰。
而抽象类可以有自己的数据成员变量,也可以有非抽象的成员方法,而且抽象类中的成员变量默认为default(本包可见),当然也可以被定义为private、protected和public,这些成员变量可以在子类中被重新定义,也可以被重新赋值。抽象类中的抽象方法(其前有abstract修饰)不能用private、static、synchronized、native等访问修饰符修饰,同时方法必须以分号结尾,并且不带花括号。所以当功能需要累积时,用抽象类。不需要累积时,用接口。 - 接口被用于实现比较常用的功能,便于日后维护或者添加删除方法。抽象类更倾向于充当公共类的角色,不适用于日后重新对里面的代码进行修改。
总结:
- 接口是一种特殊形式的抽象类,使用接口完全有可能实现与抽象类相同的操作,但一般而言,抽象类多用于在同类事物中有无具体描述的方法的场景,所以当子类和父类之间存在有逻辑上的层次结构时,推荐使用抽象类。
- 而接口多用于不同类之间,定义不同类之间的通信规则,所以当希望支持差别较大的两个或者更多对象之间的特定交互行为时,应该使用接口。
- 接口可以继承接口,抽象类可以实现接口,抽象类也可以继承具体类,抽象类也可以有静态main方法。
下列关于接口的定义中,正确的是()
- A. void methoda();
- B.public double methoda();
- C.public final double methoda();
- D.static void methoda(double d1);
- E.protected void methoda(double d1);
- F.int a;
- G.int b = 1;
答案:A、B、G。从上面分析可知,接口中的方法只能用关键字public和abstract来修饰,因此选项C、D、E都是错误的。接口中的属性默认都为public static final,由于属性被final修饰,因此它是常量,常量在定义时必须初始化。F是错误的。
下列说法中,正确的是()
- A:声明抽象方法大括号可有可无
- B:声明抽象方法不可写出大括号
- C:抽象方法有方法体
- D:abstract可修饰属性、方法和类
答案:B。抽象方法不能有方法体,同理也就不能有大括号。abstract只能用来修饰类与方法,不能用来修饰属性。
22.内部类有哪些
在Java语言中,可以把一个类定义到另外一个类的内部,在类里面的这个类就叫做内部类,外面的类就做外部类。
在这种情况下,这个内部类可以被看作外部类的一个成员(与类的属性和方法类似)。还有一种类被称为顶层(top-level)类,指的是类定义代码不嵌套在其他类定义中的类。
内部类可以分为很多种,主要有以下4种:
- 静态内部类
- 成员内部类
- 局部内部类
- 匿名内部类
静态内部类:
- 指声明为static的内部类,可以不依赖于外部类实例而被实例化,而通常的内部类需要在外部类实例化后才能实例化。
- 静态内部类不能与外部类有相同的名字,不能访问外部类的普通变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。
成员内部类:
- 静态内部类去掉static后,就成为成员内部类。
- 成员内部类为非静态内部类,可以自由地引用外部类的属性和方法,无论这些属性是静态的还是非静态的。
- 但是它与一个实例绑定在了一起,不可以定义静态的属性和方法。只有在外部的类被实例化后,这个内部类才能被实例化。
- 需要注意的是,非静态内部类中不能有静态成员。
局部内部类:
- 定义在一个代码块内的类,作用范围为其所在的代码块,是内部类中最少使用到的一种类型。
- 局部内部类像局部变量一样,不能被public、protected、private以及static修饰,只能在访问方法中定义为final类型的局部变量。
- 对一个静态内部类,去掉其声明中的static关键字,将其定义移入其外部类的静态方法或静态初始化代码段中就成为了局部静态内部类。
- 对一个成员类,将其定义移入其外部类的实例方法或实例初始化代码中就成为了局部内部类。
- 局部静态内部类与静态内部类的基本特性相同。局部内部类与内部类的基本特性相同。
匿名内部类:
- 没有类名的内部类。
- 不使用关键字class、extends、implements,没有构造函数,它必须继承其他类或实现其他接口。
- 匿名内部类的好处是代码更加简洁,紧凑,但带来的问题是易读性下降。
- 一般应用于GUI(图形用户界面)编程中实现事件处理等。
使用匿名内部类时,需要牢记以下几个原则:
- 匿名内部类不能有构造函数
- 匿名内部类不能定义静态成员、方法和类
- 匿名内部类不能是public、protected、private、static
- 只能创建匿名内部类的一个实例
- 一个匿名内部类一定是在new的后面,这个匿名类必须继承一个父类或实现一个接口。
- 因为匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。
常见笔试题:
定义如下一个外部类:
public class OuterClass{
private int d1 = 1;
//编写内部类
}
先需要在这个外部类中定义一个内部类,下面哪个定义是正确的?()
A.
class InnerClass{
public static int methoda() {
return d1;
}
}
B.
public class InnerClass{
static int methoda() {
return d1;
}
}
C.
private class InnerClass{
int methoda() { return d1; }
}
D.
static class InnerClass{
protected int methoda() {return d1;}
}
E.
abstract class InnerClass{
public abstract int methoda ();
}
答案:C、E。由于在非静态内部类中不能定义静态成员。因此A和B是错误的。由于静态内部类不能访问外部类的非静态成员,因此D是错误的。
23.如何获取父类的类名
public class Test{
public void test(){
System.out.println(this.getClass().getName());
}
public static void main(String[] args){
new Test().test();
}
}
程序运行结果为:Test
通过以上这个例子的运行结果可以得出一个结论:通过调用父类的getClass().getName()方法来获取父类的类名是可行的呢?为了解答这个问题,首先来做一个实验,给出下面的程序:
class A{}
public class Test extends A{
public void test(){
System.out.println(super.getClass().getName());
}
public static void main(String[] args){
new Test().test();
}
}
程序运行结果为:Test
为什么输出的结果不是A,而是Test呢?主要原因在于:
- Java语言中任何类都继承自Object类,getClass()方法在Object类中被定义为final与native,子类不能覆盖该方法。
- 因此this.getClass()和super.getClass()最终都调用的是Object中的getClass()方法。
- 而Object的getClass()方法的释义是:返回此Object的运行类。
- 由于在示例2中实际运行的类是Test而不是A,因此程序输出结果为Test.
那么如何才能在子类中得到父类的名字呢?
- 可以通过Java的反射机制,使用getClass().getSuperclass().getName()
- 代码如下所示:
class A{}
public class Test extends A{
public void test(){
System.out.println(this.getSuperclass().getName());
}
public static void main(String[] args){
new Test().test();
}
}
程序运行结果为:A
24.this与super有什么区别
this:
- 在Java语言中,this用来指向当前实例对象,它的一个非常重要的作用就是用来区分对象的成员变量与方法的行参(当一个方法的形参与成员变量的名字相同时,就会覆盖成员变量)。
示例如下:
class People{
String name;
public People(String name){
this.name = name;
}
}
- 构造函数使用this.name来表示左边的值为成员变量,而不是这个构造函数的形式参数。
super:
- 可以用来访问父类的方法或成员变量。
- 当子类的方法或成员变量与父类有相同名字时也会覆盖父类的方法或成员变量,要想访问父类的方法或成员变量只能通过super关键字来访问。
示例如下:
class Base{
public void f(){
System.out.println("Base:f()");
}
}
class Sub extends Base{
public void f(){
System.out.println("Sub:f()");
}
public void subf(){
f();
}
public void basef(){
super.f();
}
}
public class Test{
public static void main(String[] args){
Sub s = new Sub();
s.subf();
s.basef();
}
}
程序运行结果为:
Sub:f()
Base:f()
当子类构造函数需要显示调用父类构造函数时,super()必须为构造函数中的第一条语句,正确语法如下:
public Sub(){
super();
System.out.println("Sub");
}
25.break、continue以及return有什么区别
- break:用于直接强行跳出当前循环,不再执行剩余代码。
- continue:用于停止当次循环,回到循环起始处,进入下一次循环操作。
- return:跳转语句,用来表示从一个方法返回(返回一个值或其他复杂类型),可以使程序返回到调用该方法的地方。
- break跳出多重循环,可以在多重循环的外面定义一个标识,然后在循环体里使用带有标识的break语句,这样即可跳出多重循环。
public class Break{
public static void main(String[] args){
out:
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(j>=2)
break out;
System.out.println(j);
}
}
System.out.println("break");
}
}
程序运行结果为:
0
1
break
上例中,当内部循环执行到j=2时,程序跳出双重循环。
26.final、finally和finalize有什么区别
final、finally和finalize的区别如下:
- final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖和类不可被继承(不能再派生出新的子类)
fianl属性:
- final属性:被final修饰的变量不可变。由于不可变有两重含义:一是引用不可变;二是对象不可变。
public class Test{
public static void main(String[] arg){
final StringBuffer s = new StringBuffer("Hello");
s.append("world");
System.out.println(s);
}
}
运行结果为:Hello world
public class Test{
public static void main(String[] args){
final StringBuffer s = new StringBuffer("Hello");
s = new StringBuffer("hello world");
}
}
运行结果:编译期间错误
- final指的是引用的不可变性,只能指向初始时指向的那个对象,而不关心指向对象的内容变化。
- 被final修饰的变量必须被初始化。
- 初始化方式有以下几种:
- 在定义的时候初始化
- final成员变量可以在初始化块中初始化,但不可在静态初始化块中初始化
- 静态final成员变量可以在静态初始化块中初始化,但不可在初始化块中初始化。
- 在类的构造器中初始化,但静态final成员变量不可以在构造函数中初始化。
final方法:
- 当一个方法声明为final时,该方法不允许任何子类重写这个方法,但子类仍然可以使用这个方法。
- 还有一种被称为inline(内联)的机制,当调用一个被声明为final的方法时,直接将方法主体插入到调用处,而不是进行方法调用,能提高程序的效率。
final参数:
- final参数用来表示这个参数在这个函数内部不允许被修改。
final类:
- 当一个类被声明为final类时,此类不能被继承,所有方法都不能被重写。
- 但这不表示final类的成员变量也是不可改变的,要想做到final类的成员变量不可改变,必须给成员变量增加final修饰。
- 一个类不能既被声明为abstract,又被声明为final。
finally:
- finally作为异常处理的一部分,只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定被执行,经常被用在需要释放资源的情况下。
try{
} catch (Exception e) finally{
}
finalize:
- finalize是Object类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等。
- 一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
27.JDK中哪些类是不能继承的
- 不能继承的类是那些用final关键字修身的类。
- 一般比较基本的类型为防止扩展类无意间破坏原来方法的实现的类型都应该是final的
- 在JDK中,String、StringBuffer等都是基本类型,所以String、StringBuffer等类是不能继承的
28.assert有什么作用
断言作为一种软件调试的方法,提供了一种在代码中进行正确性检查的机制,目前很多开发语言都支持这种机制。
assert包括两种表达式,分别为:
- assert expression1
- assert expression1:expression2。
29.static关键字有哪些作用
static关键字主要有两种作用:
- 为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。
- 实现某个方法或属性与类而不是对象关联在一起,也就是说,在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。
static主要有4种使用情况:成员变量、成员方法、代码块和内部类。
static成员变量:
- Java语言中没有全局的概念,但可以通过static关键字来达到全局的效果。
- Java类提供了两种类型的变量:用static关键字修饰的静态变量和不用static关键字修饰的实例变量。
- 静态变量属于类,在内存中只有一个复制(所有实例都指向同一个内存地址)
- 只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此就可以被使用了
- 对静态变量的引用有两种方式:分别为"类.静态变量"和“对象.静态变量”。
实例变量:
- 实例变量属于对象,只有对象被创建后,实例变量才会被分配空间,才能被使用,它在内存中存在多个复制。
- 只能用"对象.实例变量"的方式来引用。
以下是静态变量与实例变量的使用示例。
package com.bigdata.springboot;
public class TestAttribute {
public static int staticInt = 0;
public int nonStaticInt = 0;
public static void main(String[] args) {
TestAttribute t = new TestAttribute();
System.out.println("t.taticInt= " + t.staticInt);
System.out.println("TestAttribute.staticInt=" + TestAttribute.staticInt);
System.out.println("t.nonStaticInt = " + t.nonStaticInt);
System.out.println("对静态变量和实例变量分别 + 1");
t.staticInt++;
t.nonStaticInt++;
TestAttribute t1 = new TestAttribute();
System.out.println("t1.staticInt=" + t1.staticInt);
System.out.println("TestAttribute.staticInt = " + TestAttribute.staticInt);
System.out.println("t1.nonStaticInt = " + t1.nonStaticInt);
}
}
程序运行结果为:
t.taticInt= 0
TestAttribute.staticInt=0
t.nonStaticInt = 0
对静态变量和实例变量分别 + 1
t1.staticInt=1
TestAttribute.staticInt = 1
t1.nonStaticInt = 0
从上例可以看出,静态变量只有一个,被类拥有,所有对象共享这个静态变量,而实例对象是与具体对象相关的。需要注意的是,在Java语言中,不能在方法体中定义static变量。
static成员方法:
- 与变量类似,Java类同时提供了static方法与非static方法。
- static方法是类的方法,不需要创建对象就可以被调用,而非static方法是对象的方法,只有对象被创建出来后才可以被使用。
- static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法。
- 因为当static方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。
- 同理,static方法也不能访问非static类型的变量。
static很重要的用途是实现单例模式。
- 单例模式的特点是该类只能有一个实例,为了实现这一功能,必须隐藏类的构造函数,即把类的构造函数声明为private,并提供一个创建对象的方法。
- 由于构造对象被声明为private,外界无法直接创建这个类型的对象,只能通过该类提供的方法来获取类的对象,要达到这样的目的只能把创建对象的方法声明为static。
程序示例如下:
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
- 用public修饰的static变量和方法本质上都是全局的,
- 若在static变量前用private修饰,则表示这个变量可以在类的静态代码块或者类的其他静态成员方法中使用,但是不能在其他类中通过类名来直接引用。
static代码块:
- static代码块(静态代码块)在类中是独立于成员变量和成员函数的代码块的。
- 它不在任何一个方法体内,JVM在加载类时会执行static代码块。
- 如果有多个static代码块,JVM将会按顺序来执行。
- static代码块经常被用来初始化静态变量
- 需要注意的是,这些static代码块只会被执行一次。
示例如下:
public class Test{
private static int a;
static {
Test.a = 4;
System.out.println(a);
System.out.println("static block is called");
}
public static void main(String[] args){
}
}
程序运行结果为:
4
static block is called
static内部类:
- static内部类是指被声明为static的内部类,可以不依赖于外部类实例对象而被实例化,而通常的内部类需要在外部类实例化。
- 静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。
示例如下:
public class Outer {
static int n = 5;
static class Inner{
void accessAttrFromOuter() {
System.out.println("Inner:Outer.n= " + n);
}
}
public static void main(String[] args) {
Outer.Inner nest = new Outer.Inner();
nest.accessAttrFromOuter();
}
}
程序运行结果如下所示:
Inner:Outer.n= 5
需要注意的是只有内部类才能被定义为static。
30.什么是实例变量?什么是局部变量?什么是类变量?什么是final变量?
实例变量:
- 变量归对象所有(只有在实例化对象后才可以)。
- 每当实例化一个对象时,会创建一个副本并初始化。
- 如果没有显示初始化,那么会初始化一个默认值
- 各个对象中的实例变量互不影响。
局部变量:
- 在方法中定义的变量,在使用前必须初始化。
类变量:
- 用static可修饰的属性、变量归类所有,只要类被加载,这个变量就可以被使用,类名.变量名。
- 所以实例化的对象共享类变量。
final变量:
- 表示这个变量为常量,不能被修改。