文章目录
注:本文是对《疯狂Java面试讲义》的小结。
局部变量和成员变量
Java的变量可分为局部变量和成员变量。
局部变量存储在栈内存中,而成员变量是对象的一部分,存储在堆内存中。
局部变量
局部变量可分为:
- 形参:
public void f1(int i) {
if (i > 0) {
......
}
}
本例中的 i
是形参,它是局部变量。
- 方法内的局部变量:
public void f2() {
int j = 0;
......
}
本例中的 j
是方法内的局部变量。
注意:方法内的局部变量在使用前必须显式初始化。
// 在定义变量时初始化
int i = 0; // OK
// 在定义变量时没有初始化,但在使用前确保其有值
int j;
j = 0; // OK
// 变量没有初始化,就直接使用,编译报错
int k;
System.out.println(k); // 编译错误
注:成员变量无需初始化,会自动赋初值(0、false、null等)。
- 代码块内的局部变量:
public class MyClass {
{
int i = 0;
......
}
......
}
注意:代码块内的局部变量和方法内的局部变量类似,在使用前必须显式初始化。
成员变量
成员变量又分为实例变量和类变量。
- 实例变量:属于实例,每个实例有各自的成员变量
- 类变量:属于类,整个类只有一个成员变量。类变量用
static
修饰
public class MyClass {
private int i; // 实例变量
private static int j; // 类变量
......
}
注: static
可以修饰类里面定义的成员,包括成员变量、方法、内部类、初始化代码块等。 static
不能修饰类、局部变量、局部内部类等。
Java变量必须“先定义,后使用”。
- 例1:实例变量的非法前向引用:
private int i = j + 1; // 编译错误
private int j = 0;
- 例2:类变量的非法前向引用:
private static int i = j + 1; // 编译错误
private static int j = 0;
- 例3:实例变量可以前向引用类变量
private int i = j + 1; // OK
private static int j = 0;
例3没有问题,这是因为类变量是随着类初始化,而实例变量是随着实例初始化,所以类变量总是在实例变量初始化之前就已经初始化好了。
注:获取Class实例的几种方法(假定有 Person
类和其 person
实例):
Class.forName("Person")
// 注意要用类名全称Person.class
person.getClass()
实例变量和类变量的初始化
实例变量的初始化
实例变量可以在以下几处做初始化操作:
- 1:定义实例变量时
- 2:初始化块(非静态)
- 3:构造器
public class Person {
// 1:定义实例变量时初始化
private String name = "Tom";
// 2:初始化块
{
name = "Jerry";
}
public String getName() {
return name;
}
// 3:构造器
public Person(String name) {
this.name = name;
}
public static void main(String[] args) {
System.out.println(new Person().getName()); // Jerry
System.out.println(new Person("LiSi").getName()); // Lisi
}
}
1(定义时初始化)和2(初始化块)比3(构造器)要更早执行。事实上,编译器会把1和2的代码放在构造器里,并且是放在最上面。
1和2的执行顺序,是按照其代码的自然顺序。
本例中1在前2在后,所以先执行1,再执行2。
注意:如果把1和2的顺序交换一下,会不会报错呢?
......
// 初始化块
{
name = "Jerry";
}
// 定义实例变量时初始化
private String name = "Tom";
......
前面提到,“初始化块”和“定义时初始化”,哪个在前就先执行哪个,那么,修改之后,在执行初始化块时,会不会报错说 name
未定义呢?
答案是没有问题。这是因为创建对象时,会先把实例变量创建出来(内存角度),然后再初始化(赋值角度)。
总结:创建对象实例时,会先把实例变量创建出来(分配内存),然后再初始化其值。其中初始化块和定义时初始化,会按其本身顺序,放到构造器的最前面来执行。
类变量的初始化
类变量的初始化类似于实例变量的初始化(只不过没有构造器):
- 定义类变量时初始化
- 初始化块(静态)
这二者,哪个在前就先执行哪个。
public class Book {
static {
price = 30.0;
}
static double price = 20.0;
public static void main(String[] args) {
System.out.println(Book.price); // 20.0
}
}
构造器
类一定有构造器(如果没有,系统会添加一个隐式的无参构造器)。
父类构造器一定会执行(从Object开始,往下一层一层的执行)。
父类构造器一定在当前类构造器的最前面执行。
可分为以下几种情况:
- 第一行代码为
super()
,显式调用父类的指定构造器 - 第一行代码为
this()
,显式调用当前类的指定构造器(在指定构造器里,还是会先执行父类构造器) - 第一行代码不调用
super()
和this()
,则在最前面隐式调用父类的无参构造器(即super()
)
注意: super()
和 this()
只能在构造器中使用,并且只能用在第一行代码。二者最多只能调用一次,且不能同时使用。
考一考:前面提到,如果构造器里没有显式调用 super()
,则会在构造器最前面隐式调用父类的无参构造器 super()
,假如父类只有有参的构造器,会怎么样呢?
答案是会编译报错。看下面的例子:
class A {
public A(String name) {
System.out.println(name);
}
}
class B extends A {
}
由于子类B没有构造器,系统会添加一个默认的无参构造器。在此构造器中,会调用父类A的无参构造器,而A没有无参构造器,所以编译报错。
父类访问子类的实例变量和方法
先看一下 this
,它指向的是对象本身。
前面提到,在创建子类对象时,一定会调用父类的构造器,如果父类构造器里面有 this,这个this指向的是谁呢?答案是子类对象。
class A {
public A() {
System.out.println("A::" + this.getClass().getName());
}
}
class B extends A {
public B() {
System.out.println("B::" + this.getClass().getName());
}
}
public class Test {
public static void main(String[] args) {
new B();
}
}
运行结果如下:
A::package1.B
B::package1.B
可见,父类构造器里的this,指向的是子类对象,这是因为new的是子类对象,父类构造器是被子类对象的构造器所调用的。
下面举一些例子。
- 例1:
class A {
private String str1 = "hi";
public void f() {
System.out.println(this.str1);
}
}
class B extends A {
private String str2 = "hello";
public void f() {
System.out.println(this.str2);
}
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
a1.f(); // hi
A a2 = new B();
a2.f(); // hello
}
}
这个例子很典型,没有任何问题。
- 例2:改为在父类构造器里调用
f()
方法:
class A {
private String str1 = "hi";
public void f() {
System.out.println(this.str1);
}
public A() {
this.f();
}
}
class B extends A {
private String str2 = "hello";
public void f() {
System.out.println(this.str2);
}
}
public class Test {
public static void main(String[] args) {
new B(); // null
}
}
奇怪的是,输出结果变成null了。
这是因为,在创建B对象时,首先调用父类的构造器,父类构造器里调用了 this.f()
方法。前面提到,this实际指向的是B对象,且 f()
方法被子类override了,所以调用的是子类的 f()
方法。而 str2
此时还没有初始化(先分配内存,再初始化,初始化是按从父类到子类的顺序操作的),所以是null值。
这个例子告诉我们,最好不要在构造器里调用实例方法。
- 例3:子类和父类同名的成员变量
class A {
private String str1 = "hi";
public void f() {
System.out.println(this.str1);
}
public A() {
this.f();
}
}
class B extends A {
private String str1 = "hello";
}
public class Test {
public static void main(String[] args) {
new B(); // hi
}
}
本例中,创建B对象时,先调用父类A的构造器,在构造器里调用 this.f()
方法,由于子类没有override该方法,所以调用的是父类的 f()
方法。问题在于,这里的 str1
是父类的还是子类的?
从实际运行结果可见,这里的 str1
是父类的。这是因为实例变量不存在override之说,this虽然指向子类,但编译期是父类类型,所以是父类的 str1
。
换句话说,在父类和子类里,实例变量即使同名,也是两个截然不同的变量,没有什么关联。父类不知道子类,更不可能知道子类的成员变量,所以在父类方法里,使用的一定是父类的成员变量。
想象一下,如果子类有成员变量 str2
,而在父类的 f()
方法里使用 str2
,显然会编译报错。
所以,获取“在父子类里同名的成员变量”时,是根据编译期的类型而定的。如果编译类型是子类,则会把父类的同名成员变量给“隐藏”掉。
这个例子告诉我们,父类成员变量和子类成员变量最好不要同名,徒增混淆。
总结:
- this指向是对象本身。构造器(包括父类和子类构造器)里的this,指向的是实际创建的对象。
- 通过this指针访问对象的实例变量时,由声明该变量的类型决定(编译期)
- 通过this指针访问对象的实例方法时,由实际的对象来决定(运行期)
考一考
下面代码的输出结果是什么?
class A {
String name = "Hi";
public void display() {
System.out.println(this.name);
}
}
class B extends A {
String name = "Hello";
@Override
public void display() {
System.out.println(this.name);
}
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
System.out.println(a1.name);
a1.display();
B b1 = new B();
System.out.println(b1.name);
b1.display();
A a2 = new B();
System.out.println(a2.name);
a2.display();
A a3 = b1;
System.out.println(a3.name);
a3.display();
}
}
答案为:
Hi
Hi
Hello
Hello
Hi
Hello
Hi
Hello
- a1:编译类型为A,实际类型也为A,所以name和display()都是A的
- b1:编译类型为B,实际类型也为B,所以name和display()都是B的
- a2:编译类型为A,实际类型为B,所以name是A的,display()是B的
- a3:编译类型为A,实际类型为B,所以name是A的,display()是B的
本例中,为了混淆,故意令A和B的成员变量名都叫 name
。虽然a3和b1实际指向同一对象,但 a3.name
和 b1.name
是不同的变量(编译类型是子类时,子类的name把父类的name隐藏掉了)。如果二者叫不同的名字,代码就会清晰一些。
方法则相对简单一些:不管编译期类型是什么,只看实际对象是什么,就调用它的方法。
super
如果在子类中显式指定 super.xxx
或 super.xxx()
,则会访问其父类的成员变量或方法。
super仍然是指向实际对象的,其编译类型是父类。
使用super,就可以访问被子类隐藏掉的同名父类成员变量,或者指定父类的方法。
父子类的类变量
- 可以通过类名来访问类变量
- 也可以通过super来访问父类的类变量
final修饰符
- final修饰变量:变量赋初值后,不能重新赋值
- final修饰方法:不能被子类override
- final修饰类:不能被继承
final修饰变量
- final修饰实例变量
- 定义变量时指定初始值
- 在初始化块(非静态)中指定初始值
- 在构造器中指定初始值
- final修饰类变量
- 定义变量时指定初始值
- 在初始化块(静态)中指定初始值
- final修饰局部变量
- 定义变量时指定初始值
注意:以实例变量为例,虽然可以在多个地方指定初始值,但是对一个变量,只能在一处指定初始值,不能在多处赋值。
用final修饰的变量,如果定义变量时指定初始值,而且这个初始值在编译期能确定下来,那么这个变量将不再是一个变量,而是会被当作“宏变量”,也就是所有出现该变量的地方,直接用其值来替换。比如:
final int i = 123;
final int j = 1 + 3;
final String str = "abc" + "def";
literal字符串
Java会将literal字符串缓存在字符串池中。
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2); // true
本例中,str1和str2指向同一个对象。
String str1 = "hello";
String str2 = "he" + "llo";
System.out.println(str1 == str2); // true
本例中,str1和str2指向同一个对象,因为str2的值在编译期可确定。
final修饰方法
final修饰的方法不能被override。
class A {
final void f() {
}
}
class B extends A {
void f() {
} // 编译错误
}
但是下面这个例子是OK的:
class A {
private final void f() {
}
}
class B extends A {
void f() {
} // OK
}
这是因为父类的private方法对子类是不可见的,所以子类可以定义同名方法(不是override,不能添加 @Override
注解)。
不过,这里的final没什么意义,因为private方法本来就无法override。
内部类中的局部变量
如果在内部类中使用局部变量,则该变量默认有final修饰。
比如:
interface A {
void f();
}
public class Test {
public static void main(String[] args) {
int i = 123;
A a = new A() {
@Override
public void f() {
System.out.println(i);
}
};
a.f(); // 123
}
}
本例中,在匿名内部类中使用了外部的局部变量i。
如果试图修改变量的值,则编译错误。比如:
interface A {
void f();
}
public class Test {
public static void main(String[] args) {
int i = 123;
new A() {
@Override
public void f() {
i++; // 编译错误
}
};
}
}
编译错误如下:
Variable 'i' is accessed from within inner class, needs to be final or effectively final
同理,如果在外部类中试图修改i的值,也会编译报错:
interface A {
void f();
}
public class Test {
public static void main(String[] args) {
int i = 123;
new A() {
@Override
public void f() {
System.out.println(i); // 编译错误
}
};
i++;
}
}
Lambda也有同样的问题:
int i = 123;
Logger.getLogger("MyLogger").info(() -> "i = " + i); // 编译错误
i++;
看来想在log里打印一下某个变量的值,还不太容易呢。
一个比较土的解决办法是,为了记log,再定义一个临时变量:
int i = 123;
int j = i;
Logger.getLogger("MyLogger").info(() -> "i = " + j);
i++;
至于为什么有此限制,是因为局部变量的作用域本来是在方法里的,但是内部类或Lambda可能产生隐式的闭包(Closure),闭包使得局部变量脱离它所在的方法而继续存在,也就是扩大了局部变量的作用域,将会引起混乱。所以Java要求内部类和Lambda所访问的局部变量必须是final的(如果不写,Java会自动加上)。