最近学习JVM,学习过程中,对某些基础的知识又有了新的认识,故得此博文。
private
尽管private
看起来已经那么熟悉,实际上还是有很多地方没有遇到过也没有思考过,这里先引用Thinking in Java中对它的描述:
The private keyword means that no one can access that member except the class that contains that member, inside methods of that class. Other classes in the same package cannot access private members, so it’s as if you’re even insulating the class against yourself.
关键有两点:
- 只能在包含该成员的类中去访问它,例如在包含该成员的类的方法
- 其余类中一律不能访问,不管以何种方式
按理来讲,这两条算是说的清清楚楚,但是由于对某些概念迷迷糊糊,导致在实际过程中,还是可能犯糊涂。
示例1
设有一下代码:
// Person.java
public class Person {
private int pri;
}
// Man.java
public class Man extends Person {
public static void main(String[] args) {
Person person = new Person();
// 以下代码会出现语法错误
person.pri = 5;
}
}
这个例子可能大部分人都能看出错误的原因了,大部分人也几乎不会犯这样的错误,不过这里引出了一些需要注意的东西,所以还是提一下。错误的原因,是对The private keyword means that no one can access that member except the class that contains that member
有误解,认为,person是拥有pri的,应当是可以访问的。当然若仔细看前面提到的那两条,就会发现,实际上这个错误非常明显。首先,第一点后面还有一句inside methods of that class
,这句话表明,你应该在包含该变量的类的方法中去访问,此处是在Man中的方法,而不是Person的方法,故有误。第二条更加直接,不能再其它的类访问,故这两条均说明了上面那样访问是有问题的。出现这样的错误,应该是对含有该变量的类和含有该变量的对象产生了混淆,前面说的是:private修饰的变量,应该在包含该变量的类里面去访问;而对象,仅仅是类的实例(实际上,在运行时,对象中的数据并不包含方法数据,方法的数据都是保存在类中,也就是方法区中),不存在”包含该变量的对象里面去访问“的说法。
这里有几点需要提一下,首先是,拥有该变量的的类(或者更准确的,声明该变量的类),这里,拥有该pri的类就是Person,然后是,访问代码所在的类,例如前面的person.pri
所在的类,就是Man;最后是访问该变量的对象所对应的类,还是以person.pri
为例,访问pri
的变量的对象person
所对应的类就是Person。
实际上,想要访问private修饰的变量,必须要前面所提到的三个类都是相同的。
示例2
// Person.java
public class Person {
private int pri;
public static void main(String[] args) {
Person person = new Person();
person.pri = 5; // 如前面所介绍的,此处访问是没有问题的
Man man = new Man();
man.pri = 10; // 根据前面介绍的,man所对应的类是Man而不是Person,因此是不能这样访问的
((Person) man).pri = 2; // ?
}
}
// Man.java
public class Man extends Person {
}
代码的?所标记的地方,其实是可以这样访问的,由于强制类型转化,将man对应的类变成了Person,所以符合前面的规则。
动态类型和静态类型
对JVM有一定了解的人(或者熟悉多态的人)都知道所谓的对象的动态类型和静态类型。静态类型,就是指编译时期确定的类型,动态类型就是指在运行时期,对象实际的类型。最典型的就是多态:
// Person.java
public class Person {
public void fun() {
System.out.println("fun in Person");
}
}
// Man.java
public class Man extends Person {
public void fun() {
System.out.println("fun in Man");
}
public static void main(String[] args) {
Person man = new Man();
}
}
以上代码是很常见的,尤其对于面向接口编程的时候,尤为广泛。man在编译时候能确定其类型为Person(严格来讲,应该是引用类型,该引用指向Person类,此处这么讲只是为了方便理解),而实际上在运行时期,它是一个Man类型(同样,严格来讲是个引用类型,这里这么说是为了方便理解)。我们还可以通过强制类型转换来转换静态类型:man = (Man)man
。动态类型并不会因为强转而改变,例如,我们将Man的main函数改为下面的代码:
public static void main(String[] args) {
Man man = new Man();
Person person = (Person) man;
man.fun();
person.fun();
}
运行代码你会发现输出为:
fun in Man
fun in Man
这说明,man的动态类型没有改变,更严格的来讲,man和person均是一个引用类型(类符号引用),man应当指向一个Man类实例(就是前面讲的,man的静态类型为Man),person应当指向一个Person类的实例(就是指person的静态类型为Person),而实际上,man和person是指向了同一个实例,即Man的实例。因此,强制类型转换,只是改变了变量应当指向某种类型的实例,但不能改变变量所指向类实例的真实类型(用前面的话来讲,强制类型转换,只是改变了静态类型,而无法改变动态类型)。
再看示例2
// Person.java
public class Person {
private int pri;
public static void main(String[] args) {
Person person = new Person();
person.pri = 5; // 如前面所介绍的,此处访问是没有问题的
Man man = new Man();
man.pri = 10; // 根据前面介绍的,man所对应的类是Man而不是Person,因此是不能这样访问的
((Person) man).pri = 2; // ?
}
}
// Man.java
public class Man extends Person {
}
我们已经知道(Pewrson)man
表明man应当指向一个Person类的实例。然而实际上,man还是指向了Man的实例,那么问题来了,既然还是Man的实例,为什么能够访问父类Person的private 变量呢?private变量不是不会被继承吗?虽然前面已经通过规则说明这样访问是没有问题的,但是,这里引出了一些问题:创建子类对象,调用父类的默认构造器究竟做了什么?父类的private变量真的不能继承吗?
首先回答第一个问题,调用父类的默认构造器究竟做了什么?
很明显,并没有创建对象,创建普通对象(特指非数组对象)需要虚拟机的new
指令(注意和Java的new关键字区分开),创建子类对象时,仅仅会调用父类的默认构造器。
那么,什么是父类的默认构造器呢?
默认构造器就是无参构造器,若一个类没有提供任何构造器,则编译器会生成一个无参构造器,若提供了一个构造器,则编译器便不会生成无参构造器,除非自己写了一个无参构造器,否则该类就没有默认构造器,对于没有默认构造器的类,其子类必须显示调用父类构造器;对有默认构造器的类,子类的构造器可以不显示调用父类的构造器,但是编译器实际上会在构造器前面加上super();
以保证调用了父类的构造器。
因此,严格来讲,创建子类对象,其实不一定是调用父类的默认构造器(毕竟父类可能没有默认构造器),但一定调用了父类的某个构造器,而父类的构造器是用来进行初始化父类的变量(非静态变量)的,但是调用父类构造器,并没有创建对象,那么那些非静态变量放在哪里呢?
只可能是子类实例里面了,那么,父类的构造器若对private变量进行初始化,表明这个private修饰的变量也应该存在!那么实际上,在子类实例里面,还是有父类的private修饰的变量的。所以,实际上,private变量也被“继承”了。但是基于前面所讲的规则,是无法在子类的方法里面访问父类的private成员变量的,于是问题2也答案也同时明确了。看起来,private成员变量也是被“继承”了,只不过这没什么用,不能读不能写,放在那里还占内存。不过至少有一个好处,那就是Java虚拟机实现上变简单了,不用考虑父类成员变量是否是private的,一股脑的全部放过来就是了,只需要在访问时,进行权限检查就行了。而权限检查是很常见的,各种调用方法和访问变量的时候都需要进行检查,因此,并没有为把父类的private变量“继承”之后进行单独的检查,所以实现上确实要比不“继承”来的方便,更加关键的,对于例子2,我们通过((Person) man).pri
就成功访问了父类的private变量,若没有把这个变量继承,反而会造成其他的问题。
小结
- 一个类若没有显示提供构造器,则编译器会提供一个无参的构造器
- 一个类若提供了构造器,则编译器不会提供构造器
- 无参构造器就是默认构造器,一个类有构造器,要么该类没有显示提供任何构造器,要么显示提供了无参构造器
- 父类的成员变量(包括private修饰的)会在子类的实例中。
- 子类的成员变量若和父类的成员变量重名也没有什么关系,虽然都放在子类实例中,但是放在了不同的地方
对于最后一点可以看看如下代码:
public class Parent {
int a;
}
public class Child extends Parent {
int a;
public void fun() {
a = 10;
super.a = 20;
System.out.println("a in child: " + a);
System.out.println("a in parent: " + super.a);
}
public static void main(String[] args) {
Child child = new Child();
child.fun();
}
}
运行代码便知。同样的,父类方法和子类方法重名时也和这种情况类似。