Java面试常见问题

各个String对象是否相等

首先明确几点:

  1. String str1 = "a";str1是常量池中内容为”a”的String对象的引用,实际对象还是在堆中(在Java中,对象都存在堆中)。常量池中如果没有指向相同常量对象的引用则创建对象并返回引用,有则不再在堆中创建新的字符串对象,直接返回其引用。
  2. String str2 = new String("a");str2是存在于堆中的String对象,只要是new就会新创建一个。
  3. String str3 = str2.intern();检查字符串常量池中是否有str2对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入常量池并返回给str3。
  4. String对象是不可变的。
1
`String str3 = new String("droid"); System.out.println(str1 == str3); `

==与equals

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。String中的equals方法是被重写过的。

抽象类和接口的区别

抽象类和抽象方法

抽象方法只有声明,而没有具体的实现(没有方法体{},不是方法体{}为空)

1
abstract void fun();

抽象方法必须用abstract关键字进行修饰。

如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。

因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。

1
2
3
public/protected abstract class ClassName {
abstract void fun();
}

抽象类就是为了继承而存在的。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。

接口

在软件工程中,接口泛指供别人调用的方法或者函数。在Java语言中,它是对行为的抽象。

接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法(且只能是public abstract方法,用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法

如果一个非抽象类实现了某个接口,就必须实现该接口中的所有方法。对于实现某个接口的抽象类,可以不实现该接口中的抽象方法。

抽象类和接口的区别

语法层面上的区别

  1. 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract方法;

  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。

  3. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

设计层面上的区别

抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将“飞行”这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将“飞行”设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,抽象类的继承是一个“是不是”的关系,而接口实现则是 “有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

再举一个例子,我们需要门具有报警alarm( )的功能,Door的open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此将Door设计为单独的一个抽象类,包含open和close两种行为,再单独将报警设计为一个接口Alarm,包含alarm()行为。再设计一个报警门继承Door类和实现Alarm接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Alram {
void alarm();
}

abstract class Door {
void open();
void close();
}

class AlarmDoor extends Door implements Alarm {
void oepn() {
//....
}
void close() {
//....
}
void alarm() {
//....
}
}

为什么Java中有些接口没有任何方法

这些没有任何方法的接口又被叫做标识接口,标识接口对实现它的类没有任何语义上的要求,它仅仅充当一个标识的作用,用来表明实现它的类属于一个特定的类型。这个标签类似于汽车的标志图标,每当人们看到一个汽车的标志图标时,就能知道这款汽车的品牌。Java类库中已存在的标识接口有Cloneable和Serializable等。在使用时会经常用instanceof来判断实例对象的类型是否实现了一个给定的标识接口。

例如,Cloneable接口,它和Serializable一样都是标记型接口,它们内部都没有方法和属性,implements Cloneable表示该对象能被克隆,能使用Object.clone()方法。如果没有implements Cloneable的类调用Object.clone()方法就会抛出CloneNotSupportedException。

Java中的clone()方法有什么作用

Java中的所有类默认都继承自Object类,而Object类中提供了一个clone()方法。

1
protected native Object clone() throws CloneNotSupportedException;
  1. 它是一个protected修饰的native方法,因此它的实现是取决于本地代码。native方法的效率一般来说都是远高于java中的非native方法。

  2. Object中的clone方法是protected的,所以要使用clone就必须继承Object类(默认)。并且为了可以使其它类调用该方法,覆写克隆方法时必须将其作用域设置为public.

  3. 克隆方法返回的是一个Object对象,所以必须要经过强制类型转换。

通常克隆对象都是通过调用super.clone()方法来获取克隆对象的,所以任何克隆的过程最终都将到达java.lang.Object 的clone()方法。但是在覆写clone()方法时,这个类需要继承Clonable接口,这个接口中没有定义方法,他类似于RandomAccess这些接口类,只做为一种标识存在。如果 clone 类没有实现 Cloneable 接口,并调用了 Object 的 clone() 方法(也就是调用了 super.Clone() 方法),那么Object 的 clone() 方法就会抛出 CloneNotSupportedException 异常。

通过从源码的注释中可以看出,Object中的clone()方法的一些特性:

  1. x.clone() != x 必须为真,也就是对于基础类型来说,其克隆后在堆中有两个独立且内容相同的内存区域。而对于引用类型来说,其引用也不相同。也就是说克隆对象和原始对象在Java堆(heap)中是两个独立的对象

  2. x.clone().getClass() == x.getClass()他们所属的类是同一个

  3. x.clone().equals(x)所比较的对象内容相同

这个方法的作用是返回一个Object对象的复制。这个复制函数返回的是一个新的对象而不是一个引用。那么怎样使用这个方法呢?以下是使用clone()方法的步骤。

  1. 实现clone的类首先需要继承Cloneable接口。Cloneable接口实质上是一个标识接口,只是说明这个类能使用Object.clone()方法,没有任何接口方法。

  2. 在类中重写Object类中的clone()方法。

  3. 在clone方法中调用super.clone()。无论clone类的继承结构是什么,super.clone()都会直接或间接调用java.lang.Object类的clone()方法。

  4. 把浅复制的引用指向原型对象新的克隆体。

在进行复制时,首先检查类有无非基本类型(即对象) 的数据成员。若没有,则返回super.clone()即可;若有,确保类中包含的所有非基本类型的成员变量都实现了深复制。

1
Object o = super.clone();//先执行浅复制

对每一个对象attr执行以下语句:

1
o.attr = this.getAttr().clone();

最后返回o。

浅复制和深复制有什么区别

浅复制(ShallowClone):被复制对象的所有变量都含有与原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象,当修改克隆后引用所指向的对象会影响原来的对象。

深复制(DeepClone):被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象。换言之,深复制把复制的对象所引用的对象都复制了一遍,当修改克隆后引用所指向的对象不会影响原来的对象。

假如定义如下一个类。

1
2
3
4
class Test {
public int i;
public StringBuffer s;
}

下图给出了对这个类的对象进行复制时,浅复制与深复制的区别。

向上转型和向下转型

什么是向上/向下转型

向上转型:子类对象转为父类,父类可以是接口。Father father = new Son();Father是父类或接口,son是子类。

向下转型:父类对象转为子类。Son son = (Son)father;

注意:父类又称超类、基类,子类又称派生类

向上转型(子->父)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Human {

public void sleep() {
System.out.println("Human sleep..");
}

public static void main(String[] args) {
Human h = new Male();// 向上转型
h.sleep();
Male m = new Male();// 没有向上转型
m.sleep();
// h.speak();此方法不能编译,Human类没有此方法
}
}

class Male extends Human {
@Override
public void sleep() {
System.out.println("Male sleep..");
}

public void speak() {
System.out.println("I am Male");
}
}

class Female extends Human {
@Override
public void sleep() {
System.out.println("Female sleep..");
}

public void speak() {
System.out.println("I am Female");
}
}

向上转型后父类的引用所指向的属性是父类的属性,如果子类重写了父类的方法,那么父类引用指向的或者调用的方法是子类的方法,这个叫动态绑定。向上转型后父类引用不能调用子类自己的方法,就是父类没有但是子类的方法,如果调用不能编译通过,比如子类的speak方法。

向下转型(父->子)

非要调用子类扩展的方法,比如speak方法,就只能向下转型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Human {
public void sleep() {
System.out.println("Human sleep..");
}

public static void main(String[] args) {
Human h = new Male();// 向上转型
Human h1 = new Human();
//h.speak();此时需要向下转型,否则不能调用speak方法。
Male m = (Male) h;
m.speak();
/**Male m1 = (Male)h1;
m1.speak(); 此时会出现运行时错误,所以可以用instanceof判断*/
// instanceof通过返回一个布尔值来指出这个对象是否是这个特定类或者是它的子类的一个实例
if (h1 instanceof Male){
Male m1 = (Male)h1;
m1.speak();

}
}
}

class Male extends Human {
@Override
public void sleep() {
System.out.println("Male sleep..");
}

public void speak() {
System.out.println("I am Male");
}
}

向下转型需要考虑安全性,如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现java.lang.ClassCastException错误。它可以使用instanceof来避免出错此类错误即能否向下转型,只有先经过向上转型的对象才能继续向下转型。

总结

向上转型:子类对象转为父类,不用强制转型。父类可以是接口,例如Queue<Node> queue = new LinkedList<>();中Queue就是接口。Father father = new Son();Father是父类或接口,son是子类。假设父类中只有sleep方法,子类中有sleep和speak方法,父类引用指向子类对象,调用子类父类都有的方法可以实现多态性(动态绑定),但是不能调用子类中特有的方法。属性还是父类的属性,方法是子类的方法。

向下转型:父类对象转为子类,需要强制转型,最好前面加一个instanceof判断是否是这个子类类型的一个示例。Son son = (Son)father;如果是子类先向上转型成父类再向下转型成子类则可以,如果是父类引用里面装的本来就是个父类对象,要转为子类则报错。

Java和C++有什么异同

Java与C++都是面向对象语言,都使用了面向对象思想(例如封装、继承、多态等),由于面向对象有许多非常好的特性(继承、组合等),因此二者都有很好的可重用性。

下面主要介绍它们的不同点:

  1. Java为解释性语言,编译成字节码由JVM解释执行,而C++为编译性语言,编译连接后直接生成可执行的二进制代码:Java为解释性语言,其运行过程为:程序源代码经过Java编译器编译成字节码,然后由JVM解释执行。而C/C++为编译型语言,源代码经过编译和链接后生成可执行的二进制代码。因此,Java的执行速度比C/C++慢,但是Java能够跨平台执行,而C/C++不能。

  2. Java纯面向对象所有代码必须写在类中,不存在全局变量或全局函数:Java为纯面向对象语言,所有代码(包括函数、变量等)必须在类中实现,除基本数据类型(包括int、float等)外,所有类型都是类。此外,Java语言中不存在全局变量或全局函数,而C++兼具面向过程和面向过程编程的特点,可以定义全局变量和全局函数。

  3. Java没有指针,更安全:与C/C++语言相比,Java语言中没有指针的概念,这有效防止了C/C++语言中操作指针可能引起的系统问题,从而使程序变得更加安全。

  4. Java不支持多继承,但是可以实现多个接口:与C++语言相比,Java语言不支持多重继承,但是Java语言引人了接口的概念,可以同时实现多个接口。由于接口也具有多态特性,因此在Java语言中可以通过实现多个接口来实现与C++语言中多重继承类似的目的。

  5. Java中不需要手动释放内存空间,提供垃圾回收器:在C++语言中,需要开发人员去管理对内存的分配(包括申请与释放),而Java语言提供了垃圾回收器来实现垃圾的自动回收,不需要程序显式地管理内存的分配。在C++语言中,通常都会把释放资源的代码放到析构函数中,Java语言中虽然没有析构函数,但却引入了一个fmalizeO方法,当垃圾回收器将要释放无用对象的内存时,会首先调用该对象的finalize()方法,因此,开发人员不需要关心也不需要知道对象所占的内存空间何时会被释放。

  6. Java具有平台无关性,对每种数据类型都分配固定长度,int总是4个字节32位。而C/C++却不然,同一个数据类型在不同的平台上会分配不同的字节数。

  7. Java不支持运算符重载,不支持预处理,不支持默认函数参数,不支持goto语句,不支持自动强制类型转换:C++语言支持运算符重载,而Java语言不支持运算符重载。C++语言支持预处理,而Java语言没有预处理器,虽然不支持预处理功能(包括头文件、宏定义等),但它提供的im­port机制与C++中的预处理器功能类似。C++支持默认函数参数,而Java不支持默认函数参数。C/C++支持goto语句,而Java不提供goto语句(但Java中goto是保留关键字)。C/C++支持自动强制类型转换,这会导致程序的不安全;而Java不支持自动强制类型转换,必须由开发人员进行显式地强制类型转换。C/C++中,结构和联合的所有成员均为公有,这往往会导致安全性问题的发生,而Java根本就不包含结构和联合,所有内容都封装在类里面。

  8. Java提供对注释文档的内建支持,所以源码文件也可以包含它们自己的文档。通过一个单独的程序,这些文档信息可以提取出来,并重新格式化成HTML。

  9. Java包含了一些标准库,用于完成特定的任务,同时这些库简单易用,能够大大缩短开发周期,例如,Java提供了用于访问数据库的JDBC库,用于实现分布式对象的RMI等标准库。C++则依靠一些非标准的、由其他厂商提供的库。

面试题:Java语言中的方法属于类中的成员吗?

答案:只有静态方法才是类的成员,非静态方法都是对象的成员

为什么需要public static void main(String[] args)这个方法

public static void main(String[] args)为Java程序的入口方法,JVM在运行程序时,会首先查找main()方法。其中,public是权限修饰符,表明任何类或对象都可以访问这个方法,static表明main()方法是一个静态方法,即方法中的代码是存储在静态存储区的,只要类被加载后,就可以使用该方法而不需要通过实例化对象来访问,可以直接通过类名.main()直接访问,JVM在启动时就是按照上述方法的签名(必须有publicstatic修饰,返回值为void,且方法的参数为字符串数组)来查找方法的入口地址,若能找到,就执行;找不到,则会报错。void表明方法没有返回值,main是JVM识别的特殊方法名,是程序的入口方法。字符串数组参数args为开发人员在命令行状态下与程序交互提供了一种手段。

因为main是程序的入口方法,所以当程序运行时,第一个执行的方法就是main()方法。通常来讲,要执行一个类的方法,先必须实例化一个类的对象,然后通过对象来调用这个方法。但由于main是程序的入口方法,此时还没有实例化对象,因此在编写main()方法时就要求不需要实例化对象就可以调用这个方法,鉴于此,main()方法需要被定义成publicstatic。下例给出了在调用main()方法时传递参数的方法。

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
}

在控制台下,使用javac Test.java指令编译上述程序,使用java Test argl arg2 arg3指令运 行程序,程序运行结果为:

1
arg1 arg2 arg3

main()方法是否还有其他可用的定义格式

  1. 由于public与static没有先后顺序关系,因此下面的定义也是合理的。

    1
    static public void main(String[] args)
  2. 也可以把main()方法定义为final。

    1
    public static final void main(String[] args)
  3. 也可以用synchronized来修饰main()方法。

    1
    static public synchronized void main (String[] args)

不管哪种定义方式,都必须保证

  1. main()方法的返回值为void
  2. main()方法的参数为字符串数组
  3. 有static与public关键字修饰
  4. 不能用abstract关键字来修饰

同一个.java文件中是否可以有多个main()方法

虽然每个类中都可以定义main()方法,但是只有与文件名相同的用public修饰的类中的main()方法才能作为整个程序的人口方法。如下例所示,创建了一个名为Test.java的文件。

1
2
3
4
5
6
7
8
9
10
class T {
public static void main(String[] args) {
System.out.println("T main");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("Test main");
}
}

static/final/abstract

static关键字

static 关键字可以用来修饰类的变量、方法、代码块和内部类。static 是静态的意思,也是全局的意思,它定义的东西,属于全局与类相关,不与具体实例相关。

  1. static用于修饰成员变量:静态变量。Java类提供了两种类型的变量:用static关键字修饰的静态变量和不用static关键字修饰的实例变量。静态变量属于类,在内存中只有一个复制(所有实例都指向同一个内存地址),只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此就可以被使用了。对静态变量的引用有两种方式,分别为“类.静态变量”和“对象.静态变量”。

    注意:不能在方法体中定义static变量。

  2. static用于修饰成员方法:静态方法。static方法中不能使用this和super关键字,不能调用非static的变量和方法,只能访问所属类的静态成员变量和成员方法。因为当static方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。

    注意:static方法不能调用非static的变量和方法

    static—个很重要的用途是实现单例模式。单例模式的特点是该类只能有一个实例,为了实现这一功能,必须隐藏类的构造函数,即把构造函数声明为private,并提供一个创建对象的方法,由于构造对象被声明为private,外界无法直接创建这个类型的对象,只能通过该类提供的方法来获取类的对象,要达到这样的目的只能把创建对象的方法声明为static,程序示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }
  3. static用于修饰代码块:静态代码块。静态代码块在JVM加载类的时候一块执行,而且它可以随意放,可以存在于该类的任何地方。static代码块(静态代码块)在类中是独立于成员变量和成员函数的代码块的。它不在任何一个方法体内,JVM在加载类时会执行static代码块,如果有多个static代码块,JVM将会按顺序来执行。static代码块经常被用来初始化静态变量。需要注意的是,这些static代码块只会被执行一次。

  4. static用于修饰内部类:静态内部类。 静态内部类是指被声明为static的内部类,它可以不依赖于外部类实例对象而被实例化,而通常的内部类需要在外部类实例化后才能实例化。静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。

    注意:只有内部类才能被定义为static,普通的类不能被定义为static。

final关键字

final 关键字有三个东西可以修饰的。修饰类,方法,变量。 详细解释一下:

(1)在类的声明中使用 final

使用了 final 的类不能再派生子类,就是说不可以被继承了。有些 java 的面试题里面,问 String 可不可以被继承。答案是不可以,因为 java.lang.String是一个 final 类。这可以保证 String 对象方法的调用确实运行的是 String 类的方法,而不是经其子类重写后的方法。

(2)在方法声明中使用 final

被定义为 final 的方法不能被重写了,如果定义类为 final 的话,是所有的方法都不能重写。而我们只需要类中的某几个方法不可以被重写,就在方法前加 final。而且定义为 final 的方法执行效率更高。

(3)在变量声明中使用 final

这样的变量就是常量了,在程序中这样的变量不可以被修改的,修改的话编译器会报错。而且执行效率也是比普通的变量要高。final 的变量如果没有赋予初值的话,其他方法就必须给它赋值,但只能赋值一次。

注意:子类不能重写父类的静态方法,也不能把父类不是静态的重写成静态的方法。想隐藏父类的静态方法的话,在子类中声明和父类相同的方法就行了。

abstract关键字

abstract可以修饰类和方法

(1)在方法声明中使用abstract

抽象方法没有方法体,即没有具体实现。

(2)在类声明中使用abstract

抽象类中可以包含抽象方法和非抽象方法

抽象类不能被实例化,也就是说你用的时候不能通过new关键字创建。

抽象类是为了被继承而创建的,抽象类中的abstract 方法必须在某个子类中重写。

abstract关键字不能和哪些关键字共存

  1. abstract不能与final关键字共存,因为final关键字修饰的类不能再派生子类,是不能被继承的。

  2. abstract不能与private关键字共存,因为私有的子类看不见无法实现继承。

  3. abstract不能与static关键字共存,当static修饰方法时,该方法可以通过类名直接调用,而abstract修饰的抽象方法是不能被直接调用的,必须在子类overriding后才能使用。

实例变量/局部变量/类变量/final变量

  • 实例变量:变量归对象所有(只有在实例化对象后才可以)。每当实例化一个对象时,会创建一个副本并初始化,如果没有显示初始化,那么会初始化一个默认值。各个对象中的实例变量互不影响。

  • 局部变量:在方法中定义的变量,在使用前必须初始化。

  • 类变量:用static可修饰的属性、变量归类所有,只要类被加载,这个变量就可以被使用(类名.变量名)。所有实例化的对象共享类变量。在 Java语言中,不能在方法体中定义static变量。

  • final变量:表示这个变量为常量,不能被修改。

内部类

内部类可以分为很多种,主要有以下4种:静态内部类(static inner class)、成员内部类(member inner class)、局部内部类(local inner class)和匿名内部类(anonymous inner class)。

  1. 静态内部类是指被声明为static的内部类,它可以不依赖于外部类实例而被实例化,而通常的内部类需要在外部类实例化后才能实例化。

    注意:静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。

    加载一个类时,其静态内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

  2. 一个静态内部类,如果去掉“static”关键字,就成为成员内部类。成员内部类为非静态内部类,它可以自由地引用外部类的属性和方法,无论这些属性和方法是静态的还是非静态的。但是它与一个实例绑定在了一起,不可以定义静态的属性和方法。只有在外部的类被实例化后,这个内部类才能被实例化。

    注意:非静态内部类中不能有静态成员。

  3. 局部内部类指的是定义在一个代码块内的类,它的作用范围为其所在的代码块,是内部类中最少使用到的一种类型。局部内部类像局部变量一样,不能被public、protected、private以及static修饰,只能访问方法中定义为final类型的局部变量。对一个静态内部类,去掉其声明中的“static”关键字,将其定义移入其外部类的静态方法或静态初始化代码段中就成为了局部静态内部类。对一个成员类,将其定义移人其外部类的实例方法或实例初始化代码中就成为了局部内部类。局部静态内部类与静态内部类的基本特性相同。局部内部类与内部类的基本特性相同。

  4. 匿名内部类是一种没有类名的内部类,不使用关键字class、extends、implements,没有构造函数,它必须继承(extends)其他类或实现其他接口。匿名内部类的好处是代码更加简洁、紧凑,但带来的问题是易读性下降。在使用匿名内部类时,需要牢记以下几个原则:

  • 匿名内部类不能有构造函数。
  • 匿名内部类不能定义静态成员、方法和类。
  • 匿名内部类不能是public、protected、private、static。
  • 只能创建匿名内部类的一个实例。
  • —个匿名内部类一定是在new的后面,这个匿名类必须继承一个父类或实现一个接口。
  • 因为匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

    例子:不使用匿名内部类来实现抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Person {
public abstract void eat();
}

class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}

public class Demo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}

使用匿名内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Person {
public abstract void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}

如何实现在main()方法执行前输出“Hello World”

众所周知,在Java语言中,main()方法是程序的入口方法,在程序运行时,最先加载的就是main()方法,但这是否意味着main()方法就是程序运行时第一个被执行的模块呢?

答案是否定的。在Java语言中,由于静态块在类被加载时就会被调用,因此可以在main()方法执行前,利用静态块实现输出“Hello World”的功能,以如下代码为例。

1
2
3
4
5
6
7
8
public class Test {
static {
System.out.println("Hello World!");
}
public static void main(String[] args) {
System.out.println("Main");
}
}

运行结果为:

1
2
Hello World!
Main

由于静态块不管顺序如何,都会在main()方法执行之前执行,因此,以下代码会与上面的代码有同样的输出结果

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
System.out.println("Main");
}
static {
System.out.println("Hello World!");
}
}

Java程序初始化的顺序是怎样的

在Java语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象。

Java程序的初始化一般遵循3个原则(优先级依次递减):1、静态对象(变量)优先于非静态对象(变量)初始化,其中,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次。2、父类优先于子类进行初始化。3、按照成员变量的定义顺序进行初始化。即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化。

Java程序初始化工作可以在许多不同的代码块中来完成(例如静态代码块、构造函数等),它们执行的顺序如下:父类静态变量(父类静态代码块,括号表示取决于书写顺序)、子类静态变量(子类静态代码块)、父类非静态变量(父类非静态代码块)、父类构造函数、子类非静态变量(子类非静态代码块)、子类构造函数。下面给出一个不同模块初始化时执行顺序的一个例子。

代码块放在成员变量前面时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base {
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类非静态代码块");
}
public static PrintMessage message1 = new PrintMessage("父类静态成员变量");
public PrintMessage message3 = new PrintMessage("父类非静态成员变量");
public Base() {
System.out.println("父类构造函数");
}
}
class PrintMessage {
public PrintMessage(String message){
System.out.println(message);
}
}

public class Derived extends Base {
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类非静态代码块");
}
public Derived() {
System.out.println("子类构造函数");
}
public static PrintMessage message2 = new PrintMessage("子类静态成员变量");
public PrintMessage message4 = new PrintMessage("子类非静态成员变量");
public static void main(String[] args) {
new Derived();
}
}

输出顺序:

1
2
3
4
5
6
7
8
9
10
父类静态代码块
父类静态成员变量
子类静态代码块
子类静态成员变量
父类非静态代码块
父类非静态成员变量
父类构造函数
子类非静态代码块
子类非静态成员变量
子类构造函数

代码块放在成员变量后面时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package array;

class Base {
public static PrintMessage message1 = new PrintMessage("父类静态成员变量");
public PrintMessage message3 = new PrintMessage("父类非静态成员变量");
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类非静态代码块");
}

public Base() {
System.out.println("父类构造函数");
}
}
class PrintMessage {
public PrintMessage(String message){
System.out.println(message);
}
}

public class Derived extends Base {
public static PrintMessage message2 = new PrintMessage("子类静态成员变量");
public PrintMessage message4 = new PrintMessage("子类非静态成员变量");
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类非静态代码块");
}
public Derived() {
System.out.println("子类构造函数");
}
public static void main(String[] args) {
new Derived();
}
}

输出顺序:

1
2
3
4
5
6
7
8
9
10
父类静态成员变量
父类静态代码块
子类静态成员变量
子类静态代码块
父类非静态成员变量
父类非静态代码块
父类构造函数
子类非静态成员变量
子类非静态代码块
子类构造函数

Java中的作用域有哪些

在计算机程序中,声明在不同地方的变量具有不同的作用域,例如局部变量、全局变量等。在Java语言中,作用域是由花括号的位置决定的,它决定了其定义的变量名的可见性与生命周期。

在Java语言中,变量的类型主要有3种:成员变量静态变量局部变量

  • 成员变量:类的成员变量的作用范围与类的实例化对象的作用范围相同,当类被实例化时,成员变量就会在内存中分配空间并初始化,直到这个被实例化对象的生命周期结束时,成员变量的生命周期才结束。
  • 静态变量:被static修饰的成员变量被称为静态变量或全局变量,与成员变量不同的是,静态变量不依赖于特定的实例,而是被所有实例所共享,也就是说,只要一个类被加载,JVM就会给类的静态变量分配存储空间。因此,就可以通过类名和变量名来访问静态变量。
  • 局部变量:局部变量的作用域与可见性为它所在的花括号内。

此外,成员变量也有4种作用域

作用域与可见性 当前类 同一package 子类 其他package
public
private × × ×
protected ×
default × ×

注意:

  1. default表明该成员变量或方法只有自己和与其位于同一包内的类可见。若父类与子类位于同一个包内,则子类对父类的default成员变量或方法都有访问权限;若父类与子类位于不同的package(包)内,则没有访问权限。

  2. 这些修饰符只能修饰成员变量,不能用来修饰局部变量。private与protec­ted不能用来修饰类(只有public、abstract或final能用来修饰类)。

一个Java文件中是否可以定义多个类

一个Java文件中可以定义多个类,但是最多只能有一个类被public修饰,并且这个类的类名与文件名必须相同,若这个文件中没有public的类,则文件名随便是一个类的名字即可。需要注意的是,当用javac指令编译这个.java文件时,它会给每一个类生成一个对应的.class文件,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public void print() {
System.out.println("Base");
}
}

public class Derived extends Base {
public static void main(String[] args) {
Base base = new Derived();
base.print();
}
}

使用javac Derived.java指令编译上述代码,会生成两个字节码文件:Base.class与Derived.class,然后使用java Derived指令执行代码,此时,控制台的输出结果为:Base。

什么是构造函数

构造函数是一种特殊的函数,用来在对象实例化时初始化对象的成员变量。在Java语言中,构造函数具有以下特点。

  1. 构造函数必须与类的名字相同,并且不能有返回值。
  2. 每个类可以有多个构造函数。当开发人员没有提供构造函数时,编译器在把源代码编译成字节码的过程中会提供一个没有参数默认的构造函数,但该构造函数不会执行任何代码。如果开发人员提供了构造函数,那么编译器就不会再创建默认的构造函数了。
  3. 构造函数可以有0个、1个或1个以上的参数。
  4. 构造函数总是伴随着new操作一起调用,且不能由程序的编写者直接调用,必须要由系统调用。构造函数在对象实例化时会被自动调用,且只运行一次;而普通的方法是在程序执行到它时被调用,且可以被该对象调用多次。
  5. 构造函数的主要作用是完成对象的初始化工作。
  6. 构造函数不能被继承,因此,它不能被覆盖,但是构造函数能够被重载,可以使用不同的参数个数或参数类型来定义多个构造函数。
  7. 子类可以通过super关键字来显式地调用父类的构造函数,当父类没有提供无参数的构造函数时,子类的构造函数中必须显式地调用父类的构造函数。如果父类提供了无参数的构造函数,此时子类的构造函数就可以不显式地调用父类的构造函数,在这种情况下编译器会默认调用父类提供的无参数的构造函数。当有父类时,在实例化对象时会先执行父类的构造函数,然后执行子类的构造函数。
  8. 当父类和子类都没有定义构造函数时,编译器会为父类生成一个默认的无参数的构造函数,给子类也生成一个默认的无参数的构造函数。此外,默认构造器的修饰符只跟当前类的修饰符有关(例如,如果一个类被定义为public,那么它的构造函数也是public)。

this/super关键字

this关键字

this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。this的用法在java中大体可以分为3种:

  1. 普通的直接引用

这种就不用讲了,this相当于是指向当前对象本身。

  1. 形参与成员名字重名,用this来区分:

    1
    this.age = age;
  2. 引用构造函数(和super一起讲)

super关键字

super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

super也有三种用法:

  1. 普通的直接引用

与this类似,super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员。

  1. 子类中的成员变量或方法与父类中的成员变量或方法同名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Country {
    String name;
    void value() {
    name = "China";
    }
    }

    class City extends Country {
    String name;
    void value() {
    name = "Shanghai";
    super.value(); //调用父类的方法
    System.out.println(name);
    System.out.println(super.name);
    }

    public static void main(String[] args) {
    City c = new City();
    c.value();
    }
    }

    运行结果:

    1
    2
    Shanghai
    China

    可以看到,这里既调用了父类的方法,也调用了父类的变量。若不调用父类方法value(),只调用父类变量name的话,则父类name值为默认值null。

    1. 引用构造函数

    super(参数):调用父类中的某一个构造函数(super(...);必须为构造函数中的第一条语句)

    this(参数):调用本类中另一种形式的构造函数(this(...);必须为构造函数中的第一条语句)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Base {
Base() {
System.out.println("父类无参构造方法");
}

Base(String name) {
System.out.println("父类含一个参数的构造方法");
}
}

public class Derived extends Base {
Derived() {
super(); // 调用父类无参构造方法
System.out.println("上面是在子类中调用super()的结果");
}

Derived(String name) {
super(name);// 调用父类具有一个String参数的构造方法
System.out.println("上面是在子类中调用super(name)的结果");
}

Derived(String name, int age, boolean male) {
this();
System.out.println("上面是在子类中调用this()的结果");
}

Derived(String name, int age) {
this(name);
System.out.println("上面是在子类中调用this(name)的结果");
}

public static void main(String[] args) {
Derived cn = new Derived();
System.out.println("============");
cn = new Derived("sunnie");
System.out.println("============");
cn = new Derived("qiqi", 18);
System.out.println("============");
cn = new Derived("haha",18,true);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
父类无参构造方法
上面是在子类中调用super()的结果
============
父类含一个参数的构造方法
上面是在子类中调用super(name)的结果
============
父类含一个参数的构造方法
上面是在子类中调用super(name)的结果
上面是在子类中调用this(name)的结果
============
父类无参构造方法
上面是在子类中调用super()的结果
上面是在子类中调用this()的结果

普通方法是否可以与构造函数有相同的方法名

可以,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public Test() {
System.out.println("constructor");
}

public void Test() {
System.out.println("Test method");
}

public static void main(String[] args) {
Test test = new Test();
test.Test();
}
}

输出结果:

1
2
constructor
Test method

什么是反射机制

反射机制是Java语言中一个非常重要的特性,它允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作。

反射机制提供的功能主要有:

  • 得到一个对象所属的类
  • 获取一个类的所有成员变量和方法
  • 在运行时创建对象
  • 在运行时调用对象的方法。

其实,反射机制非常重要的一个作用就是可以在运行时动态地创建类的对象,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package array;

class Base {
public void f() {
System.out.println("Base");
}
}

class Sub extends Base {
public void f() {
System.out.println("Sub");
}
}

public class Test {
public static void main(String[] args) {
try { // 使用反射机制加载类
Class c = Class.forName("array.Sub");
Base b = (Base) c.newInstance();
b.f();
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果为:

1
Sub

获取到Class类的三种方法:

  1. Class.forName(“类的路径”),如上例所示。
  2. 类名.class
  3. 实例.getClass()

Java创建对象的方式有几种

答案:共有4 种创建对象的方法。

  1. 通过new语句实例化一个对象。
  2. 通过反射机制创建对象。
  3. 通过clone()方法创建一个对象。
  4. 通过反序列化的方式创建对象。

组合和继承有什么区别

例如,Car表示汽车对象,Vehicle表示交通工具对象,Tire表示轮胎对象。

Car是Vehicle的一种,因此是一种继承关系(“is-a”关系):

1
2
3
4
5
6
class Vehicle {

}
class Car extends Vehicle {

}

Car包含了多个Tire,因此是一种组合关系(“has-a”关系):

1
2
3
4
5
6
class Tire {

}
class Car extends Vehicle {
private Tire t = new Tire();
}

注意:除非两个类之间是“is - a”的关系,否则不要轻易地使用继承。能使用组合就尽量不要使用继承。

多态的实现机制是什么

在Java语言中,多态主要有以下两种表现方式:

  1. 编译时多态:方法的重载(overload)。重载是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。

  2. 运行时多态:方法的覆盖(override)。子类可以覆盖父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在Java语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象。而程序调用的方法在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能确定调用哪个方法,因此通过方法覆盖实现的多态也可以被称为运行时多态,示例如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class Base {
    public Base() {
    g();
    }

    public void f() {
    System.out.println("Base f");
    }

    public void g() {
    System.out.println("Base g");
    }
    }

    class Sub extends Base {
    public void f() {
    System.out.println("Sub f");
    }

    public void g() {
    System.out.println("Sub g");
    }
    }

    public class Test {
    public static void main(String[] args) {
    Base base = new Sub();
    base.f();
    base.g();
    }
    }

输出结果为:

1
2
3
Sub g
Sub f
Sub g

由于子类Sub的f()方法和g()方法与父类Base的方法同名,因此Sub的方法会覆盖Base的方法。在执行Base b = new Sub();语句时,会调用Base类的构造函数,而在Base的构造函数中,执行了g()方法,由于Java语言的多态特性,此时会调用子类Sub的g()方法,而非父类Base的g()方法,因此会输出Sub g。由于实际创建的是Sub类的对象,后面的方法调用都会调用子类Sub的方法。

注意:只有类中的方法才有多态的概念,类中成员变量没有多态的概念。成员变量是无法实现多态的,成员变量的值取父类还是子类并不取决于创建对象的类型,而是取决于所定义变量的类型,这是在编译期间确定的。Base b = new Sub();如果Base类和Sub类中都有成员变量i,打印b.i是输出Base类中的成员变量i的值。

重载(overload)和覆盖(override)有什么区别

重载(overload)和覆盖(override)是Java多态性的不同表现方式。其中,重载是在一个类中多态性的一种表现,是指在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型。在使用重载时,需要注意以下几点:

  1. 重载是通过不同的方法参数来区分的,例如不同的参数个数、不同的参数类型或不同的参数顺序。

  2. 不能通过方法的访问权限、返回值类型和抛出的异常类型来进行重载。

  3. 对于继承来说,如果基类方法的访问权限为private,那么就不能在派生类对其重载;如果派生类也定义了一个同名的函数,这只是一个新的方法,不会达到重载的效果。

覆盖是指派生类函数覆盖基类函数。覆盖一个方法并对其重写,以达到不同的作用。在使用覆盖时需要注意以下几点:

  1. 派生类中的覆盖方法必须要和基类中被覆盖的方法有相同的函数名和参数。
  2. 派生类中的覆盖方法的返回值必须和基类中被覆盖方法的返回值相同。
  3. 派生类中的覆盖方法所抛出的异常必须和基类(或是其子类)中被覆盖的方法所抛出的异常一致。
  4. 基类中被覆盖的方法不能为private,否则其子类只是定义了一个方法,并没有对其覆盖。

重载与覆盖的区别主要有以下几个方面:

  1. 覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系
  2. 覆盖只能由一个方法或只能由一对方法产生关系;重载是多个方法之间的关系。
  3. 覆盖要求参数列表相同;重载要求参数列表不同。
  4. 覆盖关系中,调用方法体是根据对象的类型(对象对应存储空间类型)来决定;而重载关系是根据调用时的实参表与形参表来选择方法体的。

面试题:如下代码的运行结果是什么?

答案:编译错误。Sub会继承int g(),Sub又有自己的void g(),由于返回值不同所以Base中的g()没有被覆盖掉。函数只以函数名和变量个数、变量类型、变量顺序来区分的,不能以返回值来区分的,虽然父类与子类中的函数有着不同的返回值,但是它们有着相同的函数名,因此,编译器无法区分两个g()到底调用哪一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public int g() {
System.out.println("Base g");
return 0;
}
}

class Sub extends Base {
public void g() {
System.out.println("Sub g");
}
}

public class Test {
public static void main(String[] args) {
Base base = new Sub();
base.g();
}
}

如何获取父类的类名

Java语言提供了获取类名的方法:getClass().getName(),开发人员可以调用这个方法来获取类名,代码如下:

1
2
3
4
5
6
7
8
9
10
package array;

public class Test {
public void test() {
System.out.println(this.getClass().getName());
}
public static void main(String[] args) {
new Test().test();
}
}

运行结果为:

1
array.Test

试图通过super.getClass().getName()获取父类的类名:

1
2
3
4
5
6
7
8
9
10
11
package array;

class Base {}
public class Test extends Base{
public void test() {
System.out.println(super.getClass().getName());
}
public static void main(String[] args) {
new Test().test();
}
}

运行结果为:

1
array.Test

为什么输出的结果不是array.Base而是array.Test呢?主要原因在于Java语言中任何类都继承自Object类,getClass()方法在Object类中被定义为finalnative,子类不能覆盖该方法。因此this.getClass()super.getClass()最终都调用的是Object中的getClass()方法。而Ob­ject的getClass()方法的释义是:返回此Object的运行时类。由于在上面示例中实际运行的类是Test而不是Base,因此程序输出结果为Test。那么如何才能在子类中得到父类的名字呢?可以通过Java的反射机制,使用getClass().getSuperclass().getName(),代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
package array;

class Base {}
public class Test extends Base{
public void test() {
System.out.println(super.getClass().getSuperclass().getName());
}
public static void main(String[] args) {
new Test().test();
}
}

输出结果为:

1
array.Base

变量命名有哪些规则

在Java语言中,变量名、函数名、数组名统称为标识符,Java语言规定标识符只能由宇母(a〜Z,A〜Z)、数字(0〜9)、下画线(_)和$组成,并且标识符的第一个字符不能是数字。此外,标识符也不能包含空白字符(换行符、空格和制表符),不能包含保留字、关键字。在Java语言中,变量名是区分大小写的,例如Count与count被认为是两个不同的标识符,而非相同的标识符。

如何跳出多重循环

打tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package array;

public class Test {
public static void main(String[] args) {
out:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println(j);
if (j >= 2) {
break out;
}
}
}
System.out.println("I've broken out.");
}
}

设一个flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package array;

public class Test {
public static void main(String[] args) {
boolean breakFlag = false;
for (int i = 0; i < 10 && !breakFlag; i++) {
for (int j = 0; j < 10 && !breakFlag; j++) {
System.out.println(j);
if (j >= 2) {
breakFlag = true;
}
}
}
System.out.println("I've broken out.");
}
}

输出结果:

1
2
3
4
0
1
2
I've broken out.

finally/finalize

  • finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示不论是否出现异常这段语句最终一定被执行,经常被用在需要释放资源的情况下。例如释放数据库的连接资源conn.close();常放在finally中。
  • finalize是Object类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等。需要注意的是,一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,但是finalize()方法是不可靠的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的,只有在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

JDK中什么类不能被继承

不能继承的类是那些用final关键字修饰的类。一般比较基本的类型或防止扩展类无意间破坏原来方法的实现的类型都应该是final的,在JDK中,String、StringBuffer等者是基本类型,所以,String、StringBuffer等类是不能继承的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 //java.lang包中不能被继承的类:
public final class Byte
public final class Character
public static final class Character.UnicodeBlock
public final class Class<T>
public final class Compile
public final class Double
public final class Float
public final class Integer
public final class Long
public final class Math
public final class ProcessBuilder
public final class RuntimePermission
public final class Short
public final class StackTraceElement
public final class StrictMath
public final class String
public final class StringBuffer
public final class StringBuilder
public final class System
public final class Void

switch中case省略break会发生什么

switch中只可以是int/char/String类型,或者是可以隐式转换为这些类型的类型。由于byte、short和char类型的值都能够被隐式地转换为int类型,因此这些类型以及它们对应的包装类型都可以作为switch的表达式。但是,long、float、double类型不能够隐式地转换为int类型,因此它们不能被用作switch的表达式。如果一定要使用long、float或double作为switch的参数,必须将其强制转换为int型才可以。

一旦通过switch语句确定了人口点,就会顺序执行后面的代码,直到遇到关键字break。否则,会执行满足这个case之后的其他case的语句而不管case是否匹配,直到switch结束或者遇到break为止。如果在switch中省略了break语句,那么匹配的case值后的所有情况(包括default情况)都会被执行。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
int x = 4;
switch (x) {
case 1: System.out.println(x);
case 2: System.out.println(x);
case 3: System.out.println(x);
case 4: System.out.println(x);
case 5: System.out.println(x);
default:System.out.println(x);
}
}
}

输出结果:

1
2
3
4
4
4

instanceof

instanceof是Java语言中的一个二元运算符,它的作用是判断一个引用类型的变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例,即它左边的对象是否是它右边的类的实例该运算符返回boolean类型的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {}

public class Test extends Base {
public static void main(String[] args) {
Base base = new Test();
System.out.println(base instanceof Base);
System.out.println(base instanceof Test);
Base base2 = new Base();
System.out.println(base2 instanceof Base);
System.out.println(base2 instanceof Test);
Test test = new Test();
System.out.println(test instanceof Base);
System.out.println(test instanceof Test);
}
}

输出结果:

1
2
3
4
5
6
true
true
true
false
true
true

strictfp有什么作用

关键字strictfp是strict float point的缩写,指的是精确浮点,它用来确保浮点数运算的准确性。JVM在执行浮点数运算时,如果没有指定strictfp关键字,此时计算结果可能会不精确,而且计算结果在不同平台或厂商的虚拟机上会有不同的结果,导致意想不到的错误。而一旦使用了strictfp来声明一个类、接口或者方法,那么在所声明的范围内,Java编译器以及运行环境会完全依照IEEE二进制浮点数算术标准(IEEE754)来执行,在这个关键字声明的范围内所有浮点数的计算都是精确的。需要注意的是,当一个类被strictfp修饰时,所有方法都会自动被strictfp修饰。因此,strictfp可以保证浮点数运算的精确性,而且在不同的硬件平台上会有一致的运行结果。下例给出了strictfp修饰类的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
public strictfp class Test {
public static void testStrictfp() {
float f = 0.12365f;
double d = 0.03496421d;
double sum = f + d;
System.out.println(sum);
}

public static void main(String[] args) {
testStrictfp();
}
}

运行结果:

1
0.15861420949932098

Java是值传递还是引用传递

Java 到底是值传递还是引用传递? - Intopass的回答 - 知乎
https://www.zhihu.com/question/31203609/answer/50992895

基本类型和引用类型的不同之处

1
2
int num = 10;
String str = "hello";

如图所示,num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为”引用”,引用指向实际对象,实际对象中保存着内容。

赋值运算符(=)的作用

1
2
num = 20;
str = "java";

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变。
如上图所示,”hello” 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)

调用方法时发生了什么?参数传递基本上就是赋值操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
第一个例子:基本类型
void foo(int value) {
value = 100;
}
foo(num); // num 没有被改变

第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
text = "windows";
}
foo(str); // str 也没有被改变

第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。

第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。

重点理解为什么,第三个例子和第四个例子结果不同?

下面是第三个例子的图解:

builder.append("4");之后

下面是第四个例子的图解:

builder = new StringBuilder("ipad"); 之后

各种类型数据在内存中的存储方式

从局部变量/方法参数开始讲起:

局部变量和方法参数在jvm中的储存方法是相同的,都是在栈上开辟空间来储存的,随着进入方法开辟,退出方法回收。以32位JVM为例,boolean/byte/short/char/int/float以及引用都是分配4字节空间,long/double分配8字节空间。对于每个方法来说,最多占用多少空间是一定的,这在编译时就可以计算好。

我们都知道JVM内存模型中有,stack和heap的存在,但是更准确的说,是每个线程都分配一个独享的stack,所有线程共享一个heap。对于每个方法的局部变量来说,是绝对无法被其他方法,甚至其他线程的同一方法所访问到的,更遑论修改。

当我们在方法中声明一个 int i = 0,或者 Object obj = null 时,仅仅涉及stack,不影响到heap,当我们 new Object() 时,会在heap中开辟一段内存并初始化Object对象。当我们将这个对象赋予obj变量时,仅仅是stack中代表obj的那4个字节变更为这个对象的地址。

数组类型引用和对象:

当我们声明一个数组时,如int[] arr = new int[10],因为数组也是对象,arr实际上是引用,stack上仅仅占用4字节空间,new int[10]会在heap中开辟一个数组对象,然后arr指向它。

当我们声明一个二维数组时,如 int[][] arr2 = new int[2][4],arr2同样仅在stack中占用4个字节,会在内存中开辟一个长度为2的,类型为int[]的数组,然后arr2指向这个数组。这个数组内部有两个引用(大小为4字节),分别指向两个长度为4的类型为int的数组。

所以当我们传递一个数组引用给一个方法时,数组的元素是可以被改变的,但是无法让数组引用指向新的数组。

你还可以这样声明:int[][] arr3 = new int[3][],这时内存情况如下图

你还可以这样 arr3[0] = new int [5]; arr3[1] = arr2[0];

关于String:

原本回答中关于String的图解是简化过的,实际上String对象内部仅需要维护三个变量,char[] chars, int startIndex, int length。而chars在某些情况下是可以共用的。但是因为String被设计成为了不可变类型,所以你思考时把String对象简化考虑也是可以的。

String str = new String(“hello”)

当然某些JVM实现会把”hello”字面量生成的String对象放到常量池中,而常量池中的对象可以实际分配在heap中,有些实现也许会分配在方法区,当然这对我们理解影响不大。

什么是不可变类

不可变类(immutable class)是指当创建了这个类的实例后,就不允许修改它的值了。也就是说,一个对象一旦被创建出来,在其整个生命周期中,它的成员变量就不能被修改了。它有点类似于常量(const),即只允许别的程序读,不允许别的程序进行修改。在Java类库中,所有基本类型的包装类都是不可变类,例如Integer、Float等。此外,String也是不可变类。

要创建一个不可变类需要遵循下面4条基本原则:

  1. 类中所有成员变量被private所修饰。
  2. 类中没有写或者修改成员变量的方法,例如setxxx,只提供构造函数,一次生成,永不改变。
  3. 确保类中所有方法不会被子类覆盖,可以通过把类定义为final或者把类中的方法定义为final来达到这个目的。
  4. 如果一个类成员不是不可变量,那么在成员初始化或者使用get方法获取该成员变量时,需要通过clone方法来确保类的不可变性(把类成员进行深克隆)
  5. 如果有必要,可使用覆盖Object类的equals()方法和hashCode()方法。在equals()方法中,根据对象的属性值来比较两个对象是否相等,并且保证用equals()方法判断为相等的两个对象的hashCode()方法的返回值也相等,这可以保证这些对象能被正确地放到HashMap或HashSet集合中。

除此之外,还有一些小的注意事项:由于类的不可变性,在创建对象时就需要初始化所有成员变量,因此最好提供一个带参数的构造函数来初始化这些成员变量。

Java提供了哪些基本数据类型

Java语言一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean),这些数据类型不是对象,而是Java语言中不同于类的特殊类型,基本类型的数据变量在声明之后就会立刻在栈上被分配内存空间。除了这8种基本的数据类型外,其他类型都是引用类型(例如类、接口、数组等),引用类型类似于C++中的引用或指针的概念,它以特殊的方式指向对象实体,引用类型的变量在声明时不会被分配内存空间,只是存储了一个内存地址而已

字节长度:byte(1字节) = boolean(1字节) < char(2字节) = short(2字节) < int(4字节) = float(4字节) < long(8字节) = double(8字节)

以上这些基本类型可以分为如下4种类型:

  1. int长度数据类型:byte(8bit)、short(16bit)、int(32bit)、long(64bit)

  2. float长度数据类型:单精度(32bit float)、双精度(64bit double)。

  3. boolean类型变量的取值:ture、false。
  4. char数据类型:Unicode字符(16bit)。

此外,Java语言还提供了对这些原始数据类型的封装类(字符类型Character,布尔类型Boolean,数值类型Byte、Short、Integer、Long、Float、Double)。需要注意的是,Java中的数值类型都是有符号的,不存在无符号的数,它们的取值范围也是固定的,不会随着硬件环境或者操作系统的改变而改变。除了以上提到的8种基本数据类型以外,在Java语言中,还存在另外一种基本类型void,它也有对应的封装类java.lang.void,只是无法直接对它进行操作而已。

在Java语言中,默认声明的小数是double类型的,因此在对float类型的变量进行初始化时需要进行类型转换。float类型的变量有两种初始化方法:float f = 1.0ffloat f = (float)1.0。与此类似的是,在Java语言中,直接写的整型数字是int类型的,如果在给数据类型为long的变量直接赋值时,int类型的值无法表示一个非常大的数字,因此,在赋值时可以通过如下的方法来赋值:long l = 26012402244L

byte、short和char类型的值都能够被隐式地转换为int类型,long、float、double类型不能够隐式地转换为int类型。

不同数据类型的转换有哪些规则

在Java语言中,当参与运算的两个变量的数据类型不同时,就需要进行隐式的数据类型转换,转换的规则为:从低精度向高精度转换,即优先级满足byte<short<char<int<long<float<double,例如,不同数据类型的值在进行运算时,short类型数据能够自动转为int类型,int类型数据能够自动转换为float类型等。反之,则需要通过强制类型转换来实现。

在Java语言中,类型转换可以分为以下几种类型:

(1)类型自动转换。低级数据类型可以自动转换为高级数据类型,表4-3给出了常见的自动类型转换的规则。

当类型自动转换时,需要注意以下几点:

  1. char类型的数据转换为高级类型(如int,long等),会转换为其对应的ASCII码。

  2. byte、char、short类型的数据在参与运算时会自动转换为int型,但当使用“+=”运算时,就不会产生类型的转换(将在下一节中详细介绍)。

  3. 另外,在Java语言中,基本数据类型与boolean类型是不能相互转换的。

总之,当有多种类型的数据混合运算时,系统会先自动地将所有数据转换成容量最大的那一种数据类型,然后再进行计算。

(2)强制类型转换

当需要从高级数据类型转换为低级数据类型时,就需要进行强制类型转换,表4-4给出了强制类型转换的规则。

需要注意的是,在进行强制类型转换时可能会损失精度。

面试题:以下程序的输出结果是多少:

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
short a = 128 ;
byte b = (byte) a;
System.out.println(a + " "+ b);
}
}

运行结果:

1
128 -128

a = 00000000 10000000

b = 10000000

Java语言在涉及byte、short和char类型的运算时,首先会把这些类型的变量值强制转换为int类型,然后对int类型的值进行计算,最后得到的值也是int类型。因此,如果把两个short类型的值相加,最后得到的结果是int类型;如果把两个byte类型的值相加,最后也会得到一个int类型的值。如果需要得到short类型的结果,就必须显式地把运算结果转换为short类型,例如对于语句short s1 = 1; s1 = s1 + 1;,由于在运行时会首先将s1转换成int类型,因此s1+1的结果为int类型,这样编译器会报错,所以,正确的写法应该short s1 = 1; s1 = (short) (s1+1)

有一种例外情况。“+=”为Java语言规定的运算法,Java编译器会对其进行特殊处理,因此,语句short s1 = 1; s1 += 1能够编译通过,s1结果为short类型。

运算符优先级

数字越小,优先级越高。

面试题:输出结果为多少

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
byte a = 5;
int b = 10;
int c = a >> 2 + b >> 2;
System.out.println(c);
}
}

输出为0,因为+的优先级比>>高,等价于a >> (2 + b) >> 2 = a >> 12 >> 2 = 0

Math.round/Math.ceil/Math.floor

Math.round实现原理是在原来数字的基础上先增加0.5然后再向下取整,等同于(int)Math.floor(x+0.5f),Math.round(1.5)的结果为1,Math.round(-1.5)的结果为-1。

ceil,天花板,向上取整

floor,地板,向下取整

如何实现无符号数的右移操作

Java提供了两种右移运算符:>>>>>。其中,>>被称为有符号右移运算符,>>>被称为无符号右移运算符,它们的功能是将参与运算的对象对应的二进制数右移指定的位数。二者的不同点在于>>在执行右移操作时,若参与运算的数字为正数,则在高位补0;若为负数,则在高位补1。而>>>则不同,无论参与运算的数字为正数或为负数,在执行运算时,都会在高位补0。

此外,需要特别注意的是,在对char、byte、short等类型的数进行移位操作前,编译器都会自动地将数值转化为int类型,然后才进行移位操作。由于int型变量只占4 Byte(32bit),因此当右移的位数超过32bit时,移位运算没有任何意义。所以,在Java语言中,为了保证移动位数的有效性,以使右移的位数不超过32bit,采用了取余的操作,即使a >> n等价于a >> (n % 32)

String/StringBuilder/StringBuffer

  • String不能被修改,效率最低
  • StringBuilder可以被修改,线程不安全,效率最高
  • StringBuffer可以被修改,线程安全,效率第二

如果要操作的数据量比较小,应优先使用String类;如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。

length/length()/size()

  • 对于数组,length是数组的一个属性,用nums.length;
  • 对于字符串,用length()方法查看字符串的长度,str.length();
  • 对于泛型集合,用size()方法查看这个泛型集合有多少个元素,

Java中异常处理的原理是什么

异常是指程序运行时(非编译时)所发生的非正常情况或错误,当程序违反了语义规则时,JVM就会将出现的错误表示为一异常并抛出。这个异常可以在catch程序块中进行捕获,然后进行处理。而异常处理的目的则是为了提高程序的安全性与鲁棒性。

Java语言把异常当作对象来处理,并定义了一个基类(java.lang.Throwable)作为所有异常的父类。在JavaAPI中,已经定义了许多异常类,这些异常类分为Error(错误)和Excep­tion(异常)两大类。违反语义规则包括两种情况:一种是Java类库内置的语义检查,例如当数组下标越界时,会引发IndexOutOfBoundsException,当访问null的对象时,会引发NullPointerException;另一种情况是Java允许开发人员扩展这种语义检査,开发人员可以创建自己的异常类(所有异常都是Java.lang.Thowable的子类),并自由选择在何时用throw关键字抛出异常。

什么是GC

对于垃圾回收器来说,它使用有向图来记录和管理堆内存中的所有对象,通过这个有向图就可以识别哪些对象是“可达的”(有引用变量引用它就是“可达的”),哪些对象是“不可达的”(没有引用变量引用它就是不可达的),所有“不可达”对象都是可被垃圾回收的。

垃圾回收都是依据一定的算法进行的,下面介绍其中几种常用的垃圾回收算法。

  1. 引用计数算法(Reference Counting Collector)

    引用计数作为一种简单但是效率较低的方法,其主要原理如下:在堆中对每个对象都有一个引用计数器;当对象被引用时,引用计数器加1;当引用被置为空或离开作用域的时,引用计数减1,由于这种方法无法解决相互引用的问题(如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,即使子对象和父对象都不再使用了,他们的引用计数永远不可能为0),因此JVM没有采用这个算法。  

  2. 追踪回收算法(Tracing Collector)或标记-清除算法

    追踪回收算法利用JVM维护的对象引用图,从根结点开始遍历对象的引用图,同时标记遍历到的对象。当遍历结束后,未被标记的对象就是目前已不被使用的对象,可以被回收了。

  3. 压缩回收算法(Compacting Collector) 或标记-整理算法

    标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

  4. 复制回收算法(Copying Collector)

    复制回收算法的主要思路如下:把堆分成两个大小相同的区域,在任何时刻,只有其中的一个区域被使用,直到这个区域的被消耗完为止,此时垃圾回收器会中断程序的执行,通过遍历的方式把所有活动的对象复制到另外一个区域中,在复制的过程中它们是紧挨着布置的,从而可以消除内存碎片。当复制过程结束后程序会接着运行,直到这块区域被使用完,然后再采用上面的方法继续进行垃圾回收。

    这个算法的优点是在进行垃圾回收的同时对对象的布置也进行了安排,从而消除了内存碎片。但是这也付出了很高的代价:对于指定大小的堆来说,需要两倍大小的内存空间;同时由于在内存调整的过程中要中断当前执行的程序,从而降低了程序的执行效率。

  5. 按代回收算法(Generational Collector)

    复制回收算法主要的缺点如下:每次算法执行时,所有处于活动状态的对象都要被复制,这样效率很低。由于程序有“程序创建的大部分对象的生命周期都很短,只有一部分对象有较长的生命周期”的特点,因此可以根据这个特点对算法进行优化。按代回收算法的主要思路如下:把堆分成两个或者多个子堆,每一个子堆被视为一代。算法在运行的过程中优先收集那些“年幼”的对象,如果一个对象经过多次收集仍然“存活”,那么就可以把这个对象转移到高一级的堆里,减少对其的扫描次数。

谢谢小天使请我吃糖果
0%