java变量大体可以分为局部变量和成员变量:
局部变量可分为:形参、方法内的局部变量,代码块内的局部变量,它们作用时间短暂,被存在方法栈内存中
成员变量是在类体内定义的变量:根据static修饰符来区分非静态变量或实例变量,和静态变量或类变量
PS:static的作用就是将实例成员变成类成员,static只能修饰在类里定义的成员部分,包括成员变量、方法、内部类、初始化块、内部枚举类。如果没有使用static修饰这些类里的成员,则这些成员属于类的实例,否则属于类本身。所以static不能修饰外部类、局部变量、局部内部类。
对于实例变量,程序每初始化一个class对象,JVM就会分配一块内存给实例变量,而对于类变量,一旦初始化之后,则不再分配内存,所有实例对象共用一块内存。所以通过实例对象访问类变量时,最终会转成通过class来访问类变量,因为它属于类本身
从语法的角度来看实例变量和类变量的初始化:
实例变量初始化:可以在定义实例变量时指定初始值、在非静态初始化块中指定初始值和在构造器中指定初始值
类变量初始化:可以在定义类变量是指定初始值和静态初始化块中指定初始值
那么初始化的时候很明显了,
对于实例变量,首先是定义实例变量初始化或非静态初始化块初始化(看代码中的顺序),然后再导构造器初始化
从下面编译后的代码可知,所有的实例变量初始化操作,最终都是在构造器中完成的
public class HelloWorld {
String name = "12";
{
name = "123";
}
public HelloWorld(){
System.out.println(name);
}
}
//编译完成后,使用javap命令查询编译后的代码
//可以看到下面第5和第11行中,对类变量执行初始化,顺序保持和代码中的顺序
~java>javap -c HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld {
java.lang.String name;
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String 12
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: ldc #4 // String 123
13: putfield #3 // Field name:Ljava/lang/String;
16: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_0
20: getfield #3 // Field name:Ljava/lang/String;
23: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: return
}
对于类变量,就是定义类变量或者静态初始化块,具体看代码中的顺序
从下面编译后的代码可知,所有的类变量初始化操作,最终都是在静态初始化块中完成的
public class HelloWorld {
static {
name = "123";
}
static String name = "12";
public HelloWorld(){
System.out.println(name);
}
}
//下面是对类变量进行初始化, 可以看到类变量的初始化没有放在构造器中了
//而是放在static初始化块中了,初始化顺序也是和代码中的顺序保持一致
~java>javap -c HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld {
static java.lang.String name;
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: getstatic #3 // Field name:Ljava/lang/String;
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
static {};
Code:
0: ldc #5 // String 123
2: putstatic #3 // Field name:Ljava/lang/String;
5: ldc #6 // String 12
7: putstatic #3 // Field name:Ljava/lang/String;
10: return
}
构造器中的this代表的是正在初始化的java对象,假如正在初始化的是类本身,那么this在编译时类型和运行时类型都代表类本身,但假若正在初始化的是其他类,那么this在编译时类型代表类本身,在运行时类型代表的是其他类
下面程序可以看出this的真面目
public class HelloWorld {
public static void main(String[] args) {
new Dog();
}
}
class Animal {
private int i=2;
public Animal(){
System.out.println(this.i);
this.show();
System.out.println(this.getClass());
}
public void show(){
System.out.println(i);
}
}
class Dog extends Animal {
private int i = 22;
public Dog(){
i=222;
}
@Override
public void show(){
System.out.println(i);
}
}
//最后输出
2
0
class Dog
大家都知道,如果c继承了p,假若在修饰符允许的情况下,那么c可以访问p的成员变量和方法,但是,继承的时候,成员变量和方法的处理是不一样的,下面看示例代码:
//编译Dog.java文件, 查看编译后的文件信息,可以看到Animal父类的方法show被移到Dog类中
// 而Animal中的成员变量i并没有迁移到Dog中
public class Dog extends Animal {
private int i = 22;
}
class Animal {
private int i=2;
public void show(){
System.out.println(i);
}
}
//编译Dog.java文件, 查看编译后的文件信息,可以看到Animal父类的方法show被移到Dog类中
// 而Animal中的成员变量i并没有迁移到Dog中
java>javap -c -private Dog
Compiled from "Dog.java"
public class Dog extends Animal {
private int i;
public Dog();
Code:
0: aload_0
1: invokespecial #1 // Method Animal."":()V
4: aload_0
5: bipush 22
7: putfield #2 // Field i:I
10: return
public void show();
Code:
0: aload_0
1: invokespecial #3 // Method Animal.show:()V
4: return
}
有了上面的说明,那么我们在实例化Animal和Dog之后,分别调用成员变量i,可以得到值2和22,而调用方法show时则会得到2和2,所以从内存控制上来看,Dog在初始化的时候会给成员变量i分配两块内存,记住是两块,假如LittleDog再继承了Dog,那么就是3块内存,而show方法只会分配一块内存;
那么考虑下面问题,在Dog中重写show方法,代码保持和Animal一样打印成员变量i,那么实现如下代码:
Dog dog = new Dog(); Animal animal = dog; dog.show(); animal.show(); System.out.println("访问Dog:" + dog.i); System.out.println("访问Animal:" + animal.i);
程序将输出什么呢?
答案是:
22
22
访问Dog:22
访问Animal:2
由此可以得出下面结论:对于一个引用类型变量而言,当它访问引用的对象的实例变量时,该实例变量的值取决于声明该实例变量是的类型,当它访问对象的方法时,该方法的行为取决于它实际引用的对象的类型;
所以继承成员变量和继承方法的区别在于,当他们被访问时,成员变量的值属于声明该变量的对象,方法的调用属于实际引用它的对象,在内存分配上,成员变量会另外分配内存,而继承方法不会,除非方法被重写,或者不允许继承