以下内容是从任小龙讲师课堂笔记中整理。
JVM内存模型
如上图JVM内存是人为根据不同内存空间的存储特点以及存储的数据进行划分的。
程序计数器:当前线程所执行的字节码行号指示器
本地方法栈:为虚拟机使用的native方法服务
Java虚拟机栈:描述Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法会创建一个栈帧,栈帧存放了当前方法的数据信息(局部变量),当方法执行完毕栈帧就被销毁。
Java堆:被所有线程共享的一块内存区域,在虚拟机启动时创建,所有的对象实例及数组都要在堆上分配。(使用new关键字表示在堆中开辟一块新的存储空间)
方法区:线程共享的内存区域,存储已被虚拟机加载的类信息,常量,静态变量以及代码编译后的代码数据(字节码)。
Q:如下代码堆栈执行顺序是怎样的?
public void main(){ a(); b(); } public void a(){} public void b(){ c(); } public void c(){}
A:
数组初始化
new关键字:在堆空间开辟一块内存区域,用来存储数据。
nums引用于堆空间中内存地址0x1234这块区域,表面上操作nums,底层操作0x1234这块区域
如果堆中的内存空间没有被引用时,就变成了垃圾,等待垃圾回收器回收。
参数的值传递机制--基本数据类型
public class test { public static void main(String[] args) { int x =10; System.out.println("main方法前x=" + x);//10 change(x); System.out.println("main方法后x=" + x);//10 } public static void change(int x) { System.out.println("change方法前x=" + x);//10 x = 50; System.out.println("change方法后x=" + x);//50 } }
没有new关键字不用开辟堆内存空间
change方法执行完毕,change方法栈帧退出,但是main方法栈帧仍留在栈中,main方法栈帧的x变量值不变。
参数的值传递机制--引用数据类型
public class test { public static void main(String[] args) { int[] arr = new int[]{10,99}; System.out.println(arr[0] + "," + arr[arr.length -1]);//10,99 swap(arr); System.out.println(arr[0] + "," + arr[arr.length -1]);//99,10 } public static void swap(int[] arr) { int temp = arr[0]; arr[0] = arr[arr.length - 1]; arr[arr.length - 1] = temp; } }
new关键字会在堆中开辟一片内存区域,堆是共享的。当swap方法栈帧对0x1234内存区域修改之后退出栈,main方法栈帧指向的0x1234与swap中指向的0x1234是同一块内存区域,所以arr的引用已经被修改了。
对象实例化过程--内存分析
public class Servant { public String name; public int age; }
public class test { public static void main(String[] args) { Servant s1 = new Servant(); s1.name = "Lily"; s1.age = 18; Servant s2 = new Servant(); s2.name = "Lucy"; s2.age = 20; s1 = s2; } }
有方法就有栈,有new就有堆,有类就有方法区
static修饰符
1.static修饰的方法、字段和内部类是类级别的,而不是对象级别的。随所在类的加载而加载,当JVM把字节码加载进JVM的时候,static修饰的成员同时被加载至内存中。
2.static修饰的成员要优先于对象,对象是我们手动通过new关键字创建出来的。
3.static修饰的成员被该类型的所有对象所共享。
表面上使用对象访问static成员,实际上编译后使用类名访问的。
public class Person{ String name; int age; static int totalNum = 5; public void die(){ totalNum--; System.out.println("去世"); } public void destory(){ totalNum = 0; System.out.println("人类灭亡"); } public Person(String n,int a){ name = n; age = a; totalNum++; } }
public class test { public static void main(String[] args) { System.out.println(Person.totalNum); Person p1 = new Person("Lily",18); System.out.println(Person.totalNum); Person p2 = new Person("Lucy",20); System.out.println(Person.totalNum); p1.destory(); System.out.println(Person.totalNum); } }
类成员与实例成员的访问:
类中的成员:字段,方法,内部类
类成员:使用static修饰的成员
实例成员:没有使用static修饰的成员
类成员只能访问类成员,实例成员只能访问实例成员(实例可以访问类成员,但是底层依然使用类名访问)。
static import:使用静态方法时,可以省略类名,实际开发中不使用静态导入,因为分不清方法属于的类,而且不同的类中可能存在相同的方法。
权限访问修饰符
private:只能在本类中访问,离开本类后不能直接访问
缺省:访问者的包必须和当前类所属包相同才可以访问
protected:表示子类访问权限,同包中的类可以访问,子类也可以访问。
public:当前工程任何地方都可以访问
继承
在Java语言中,存在多个类的时候我们使用extends关键字表示子类和父类之间的关系。
语法格式:
public class 子类类名 extends 父类类名
{
//TODO 编写自己特有的状态和行为
}
在Java中,类和类之间的继承关系只允许单继承,不允许多继承,即A只能有一个直接的父类,不能出现A同时继承类B和类C
,但是Java允许多重继承。
继承解决了代码重复问题。
子类只是父类的一个特殊存在,使用'IS A'判断是否可以继承。
1.如果父类的成员使用public修饰,子类继承
2.如果父类的成员使用protected修饰,子类也继承,即使父类和子类不在同一个包中
3.如果父类和子类在同一个包中,此时子类可以继承父类中缺省修饰符的成员
4.如果父类中的成员使用private修饰,子类打死都不继承,因为private只能在本类中访问。
5.父类的构造器子类不能继承,因为构造器必须和当前类名相同。
方法覆盖:override
子类重写父类中的方法,父类必须已经存在该方法。
方法重写和方法重载有什么区别?
方法重写和方法重载没有任何关系,只是名字类似而已。
方法重载:overload
作用:解决同一个类中功能相同,方法名不同不同的问题。(既然功能相同,方法名应该也相同)
规则:两同一不同
功能相同,方法名相同,方法参数列表不同(参数类型,参数个数,参数顺序)
方法重写:override
作用:解决子类继承父类之后,可能父类的某一个方法不满足子类的具体特征,此时需要在子类中重写定义该方法,并重写方法体。
在子类的方法中调用父类被覆盖的方法,使用super.方法名
子类初始化过程
子类初始化过程:创建子类对象的过程
在创建子类对象之前,会先创建父类对象。
调用子类构造器之前,在子类构造器中会首先调用父类构造器,默认调用父类无参数构造器。
如果父类不存在可以被子类访问的构造器,则不能存在子类。
public class Animal { private String name; private int age; public Animal(){ System.out.println("Animal 构造器"); } }
public class Fish extends Animal { private String color; public Fish(){ System.out.println("Fish构造器"); } }
public class test { public static void main(String[] args){ Fish f = new Fish(); } }
super关键字使用场景:
1.可以使用super解决子类隐藏父类字段的情况(字段名相同,类型可以相同,也可以不同),该情况一般不讨论,破坏封装性。
2.在子类中调用父类被覆盖的方法。
3.在子类构造器中调用父类构造器,必须使用super语句:super([实参])
隐藏:
1.满足继承的访问权限下,隐藏父类静态方法,若子类定义的静态方法签名与父类的静态方法签名相同,那么此时就是隐藏父类静态方法。仅仅是静态方法。
2.满足继承的访问权限下,隐藏父类字段,若子类定义的字段与父类的字段名相同(不管类型),那么此时就是隐藏父类字段,此时只能通过super访问被隐藏的字段。
3.隐藏本类中字段,如同类中某局部变量与字段名相同,此时就是隐藏本类字段,只能通过this访问被隐藏字段。
多态
Animal a = new Dog();
对象具有两种类型:
编译类型:声明对象变量的类型。
运行类型:对象的真实类型。
编译类型必须是运行类型的父类或相同。
当编译类型和运行类型不同时多态就出现了。
所谓多态:对象具有多种形态,对象可以存在不同的形式。
如:
Animal a = null;
a = new Dog();//a此时表示Dog类型的形态
a = new Cat();//a此时表示Cat类型的形态
多态的前提:可以是继承关系(类和类),也可以是实现关系(接口和实现类),在开发中,多态一般指第二种。
多态的特点:把子类对象赋给父类变量,在运行时期会表现出具体的子类特征。
Q:多态有什么好处?
public class Animal { public void eat(){ System.out.println("Animal eat"); } }
public class Dog extends Animal { @Override public void eat(){ System.out.println("Dog eat"); } }
public class Cat extends Animal { @Override public void eat(){ System.out.println("Cat eat"); } }
//使用多态前 public class Person { public void feed(Dog dog){ System.out.println("feeding . . ."); dog.eat(); } public void feed(Cat cat){ System.out.println("feeding . . ."); cat.eat(); } }
//使用多态后 public class Person { public void feed(Animal animal){ System.out.println("feeding . . ."); animal.eat(); } }
public class test { public static void main(String[] args){ Person p = new Person(); Dog d = new Dog(); p.feed(d); Cat c = new Cat(); p.feed(c); } }
A:
多态的好处:当不同子类的对象当做父类类型看待时,可以屏蔽不同子类之间的实现差异,从而写出通用代码达到通用编程,以适应不断变化的需求。
多态的方法调用
示例1
class SuperClass{ public void doWork(){ System.out.println("doWork"); } } class SubClass extends SuperClass{ } public class test { public static void main(String[] args){ SuperClass s = new SubClass(); s.doWork(); } }
先在子类中查找doWork方法,子类中查找不到,再从父类中查找。
上述例子执行结果:编译通过,执行父类中的doWork方法
示例2
class SuperClass{ } class SubClass extends SuperClass{ public void doWork(){ System.out.println("doWork"); } } public class test { public static void main(String[] args){ SuperClass s = new SubClass(); s.doWork(); } }
上述例子编译失败。
编译时期去编译类型中查找方法,如果不存在,则编译失败,如果存在则编译成功。
示例3
class SuperClass{ public void doWork(){ System.out.println("SuperClass doWork"); } } class SubClass extends SuperClass{ public void doWork(){ System.out.println("SubClass doWork"); } } public class test { public static void main(String[] args){ SuperClass s = new SubClass(); s.doWork(); } }
如果父类和子类中都存在该方法,则编译通过。运行时期则执行运行类型中的方法。
示例4
Class SuperClass{ public static void doWork(){ System.out.println("SuperClass doWork"); } } class SubClass extends SuperClass{ public static void doWork(){ System.out.println("SubClass doWork"); } } public class test { public static void main(String[] args){ SuperClass s = new SubClass(); s.doWork(); } }
编译通过,执行SuperClass中的doWork方法。静态方法的调用只需要有类即可,如果使用对象调用静态方法,其实是使用编译类型来调用静态方法(或者说其实底层使用类名调用),和对象没有任何关系。这种情况称为隐藏,不叫方法覆盖。
引用类型转换
基本类型转换:
自动类型转换:把小类型数据赋值给大类型变量
byte b =12;
int i = b;
强制类型转换:把大类型数据赋值给小类型变量
引用类型转换:
引用类型的大和小指的是父类和子类的关系
自动类型转换:把子类类型赋值给父类变量(多态)
Animal a = new Dog();
强制类型转换:把父类类型赋给子类变量,但是该父类类型变量的真实类型应该是子类类型。
Animal a = new Dog();
Dog b = (Dog)a;
instanceof运算符:判断该对象是否为某一个类的实例。
语法格式: boolean b = 对象A instanceof 类C;
若对象是类的实例,则返回true
若对象是父类的实例,也返回true。
在开发中,有时候,我们只想判断真实类型的实例,而不想判断编译类型的实例,可以使用对象的getClass方法和类的class字段比较
如 对象A.getClass == 类B.class
字段不存在多态
class SuperClass{ public String name = "Super"; public static void doWork(){ System.out.println("SuperClass doWork"); } } class SubClass extends SuperClass{ public String name = "Sub"; public static void doWork(){ System.out.println("SubClass doWork"); } } public class test { public static void main(String[] args){ SuperClass s = new SubClass(); System.out.println(s.name); s.doWork(); } }
输出结果为
Super
SuperClass doWork
通过对象调用字段,在编译时期就决定了调用哪一块内存的数据,----字段不存在覆盖的概念,不能有多态特征。
当子类和父类存在相同字段的时候,无论修饰符是什么,都会在各自的内存空间中存储数据。