成员变量初始化
1.成员变量分为两种
(1)静态变量,又称为类变量。它是用static修饰的。
(2)实例变量,就是非静态成员变量,它是没有static修饰的。
这两种成员变量的初始化位置和时机都是不同的。
2.静态变量的初始化
(1)什么时候进行的静态变量的初始化?
在类加载之后,对类进行初始化时发生的。
当第一次使用某个类时,如果发现这个类没有加载到内存,那么会先将它加载到内存。
(2)初始化的执行特点
一个类的初始化只会发生一次。只执行一次。
如果子类初始化时,发现父类没有初始化,会先初始化父类,再初始化子类;如果父类初始化过了,就不会再初始化父类了。
(3)哪些代码负责静态变量的初始化
①静态变量声明时的显式赋值
②静态代码块中的语句
class Demo{
static int a = 1;//静态变量显式赋值
static int b;
static{
//静态代码块
System.out.println("a = " + a);//1
System.out.println("b = " + b);//0
// System.out.println("c = " + c);//这样语法检查不通过,编译器本身也是程序,它的逻辑当中会认为这样是错误的
System.out.println("c = " + Demo.c);//可以通过"类名."进行访问
a = 2;
b = 2;
}
static int c = 1;
static{
System.out.println("c = " + c);
}
}
(4)底层的原理:一个类在初始化时,本质上是执行<clinit>()方法
cl:class 类
init:initialize 初始化
clinit()方法不是程序员手动声明,它是由编译器根据程序员写的上面的A,B两部分代码“按顺序”组装而成的。 也就是说上面的Demo类在编译后会变成:
class Demo{
//静态变量的声明
static int a;
static int b;
static int c;
void clinit(){
a = 1;
System.out.println("a = " + a);//1
System.out.println("b = " + b);//0
System.out.println("c = " + Demo.c);//可以通过"类名."进行访问,0
a = 2;
b = 2;
c = 1;
System.out.println("c = " + c);
}
}
实例变量初始化
1.什么时候进行的实例变量初始化?
在new对象时
2.初始化的执行特点。
(1)每次new对象都要执行
(2)子类实例初始化时也会执行父类实例初始化相关的代码
子类的构造器中一定会调用父类的构造器,其中本质上就是调用父类中实例变量初始化相关的代码。
3.哪些代码负责实例变量的初始化
(1)实例变量声明时的显式赋值表达式
(2)非静态代码块
(3)构造器
①:super(),super(实参列表),this(),this(实参列表),如果构造器没有写上面的代码,默认时super()
②:构造器中剩下的代码。
(4)底层的原理
每一次new对象时,实际上是执行对应的()方法
<init>方法不是由程序员手动声明的,而是根据上面的(1)(2)(3)部分代码组装而成的:
Ⅰ.组装上面的(3)①
Ⅱ.组装上面的(1)(2),他俩按照编写的顺序组装
Ⅲ.组装上面的(3)②
注意:我们如果给某个类编写了多个构造器,那么将会有多个<init>方法,即有几个构造器,就有几个<init>方法。它们的形参列表不同。组装时,上面(3)①和(3)②根据不同的构造器,放到对应的<init>方法中,上面(1)和(2)每一个放一份。
class Data{
int a = 1;//实例变量声明时的显式赋值表达式 a=1
{
//非静态代码块,又称为构造块,实际开发中很少使用
System.out.println("Data非静态代码块1");
System.out.println("a = " + a);
// System.out.println("b = " + b);//编译报错,编译器检查语法时,是遵循“前向引用”
System.out.println("b = " + this.b);
a = 2;
}
Data(){
a = 3;
b = 3;
System.out.println("无参构造");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
int b = 1;
{
//非静态代码块
System.out.println("非静态代码2");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
class SubData extends Data{
{
//非静态代码块,又称为构造块,实际开发中很少使用
System.out.println("SubData非静态代码块");
}
}
例如:上面的Data类组装后是这样的
class Data{
int a;
int b;
void /<init>(){
super();//省略了,也是存在的,它是访问默认父类的实例初始化代码,默认父类是Object类
a= 1;
//非静态代码块,又称为构造块,实际开发中很少使用
System.out.println("Data非静态代码块1");
System.out.println("a = " + a);
// System.out.println("b = " + b);//编译报错,编译器检查语法时,是遵循“前向引用”
System.out.println("b = " + this.b);
a = 2;
b = 1;
//非静态代码块
System.out.println("非静态代码2");
System.out.println("a = " + a);
System.out.println("b = " + b);
a = 3;
b = 3;
System.out.println("无参构造");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
执行顺序
当我们第一次使用某个类时,就是在new对象,那么会先完成类的初始化,然后在进行实例初始化。 (1)父类的初始化(只会执行一次)
(2)子类的初始化(只会执行一次)
(3)父类对应的实例初始化代码
(4)子类对应的实例初始化代码
先后顺序:
父类静态代码块
子类静态代码块
父类构造代码块
父类构造方法
子类构造代码块
子类构造方法
多态
1.什么是多态?
字面上来说,多种形态。
在Java中,多态是指,某个对象(准确来说是变量)表现为编译时
2.如何才能体现出多态?
多态引用:
父类的类型 变量名 = 子类的对象;
此时这个变量,就会出现编译时类型与运行时类型不一致的情况。
3.表现结果?
编译时,该变量按照父类类型进行编译,只能调用父类中声明的方法等成员。
运行时,按照子类类型进行运行,方法被子类重写过的话,是执行子类重写的代码。
4.多态有哪些应用?
多态的应用在于:
(1)多态的数组
声明数组时,元素的类型指定为父类类型,实际存储的对象是子类对象。
Person[] array = new Person[5];
array[0] = new Woman(); //左边的array[0]的类型是Person类型,右边是实际存储的对象,Woman对象
(2)多态的参数
声明方法时,形参的类型声明为父类类型,实际调用方法时,传入的实参是子类的对象。
声明:public void method(Person person){...} (Person person)形参
调用:method(new Woman()); (new Woman())实参
(3)多态的返回值
方法的返回值类型声明为父类的类型,实际返回值时子类的对象。
声明:public Person method(){
...
return new Woman();
}
(4)总结:多态可以使代码更灵活,功能更强大。
面向对象三个基本特征(封装、继承、多态)的好处
1.封装:安全、隐藏细节、简便
2.继承:代码复用性提高、代码扩展能力提高
3.多态:使得代码更灵活,使用更方便。
向上转型和向下转型
1.什么是向上与向下转型?
他们都是属于类型的转换。
向上转型:把一个对象/变量类型自动提升为父类的类型。
向下转型:把一个对象/变量类型强制类型转换为子类的类型。
注意:向上转型与向下转型都只是针对编译时类型来说的,运行时类型不会改变。
Java的基本数据类型的转换:
(1)自动类型转换‘
byte->short->int->long->float->double
char->
(2)强制类型转换:
double->float->long->int->short->byte
char->
2.向上转型
(1)什么情况下会发生向上转型?
当把子类对象/子类类型变量赋值给父类的变量时,自动就会升级为父类的类型,就是多态引用时就发生了向上转型。
(2)向上转型后有什么问题?
通过父类的变量
3.向下转型
(1)什么情况下会发生向下转型?
当如果某个子类的对象想要调用子类“扩展”的成员时,就必须对刚刚向上转型的变量进行向下转型
(2)向下转型后有什么问题?
可能发生ClassCastException:类型转换异常
4、如何避免向下转型报错?
可以使用instanceof关键字进行判断后再转型
5、哪些情况下instanceof关键字判断是返回true的呢?
变量/对象 instanceof 类型
变量/对象的运行时类型 <= 类型就返回true。
方法的分类(虚方法与非虚方法)
之前学习方法,说分为:静态方法和非静态方法。
现在说方法,从底层分为:
1.非虚方法:编译期间就能确定调用哪个的方法
A:静态方法
B:私有方法等
C:final方法
/*
结论:对于非虚方法来说,只看编译时类型。
*/
public class TestMethod {
public static void main(String[] args) {
Base base = new Sub();
base.method();//base编译时类型是Base,运行时类型是Sub类型
//这么写容易误解,以为调用的是子类重写的method方法
//为了避免这些误会,建议静态方法使用“类名."进行调用
Sub.method();//执行子类的method
Base.method();//执行父类的method
Base.test();//执行父类的test
Sub.test();//执行父类的test 静态方法可以继承到子类,但是不能重写
}
}
class Base{
public static void method(){
System.out.println("父类静态方法");
}
public static void test(){
System.out.println("父类test静态方法");
}
}
class Sub extends Base{
/*
method()方法不是重写父类的方法,它是相当于子类自己的静态方法,和父类没什么关系
*/
// @Override //建议重写的方法上面都加上@Override,编译器会帮你校验重写方法是否满足它的要求,不满足要求就会报错
public static void method(){
System.out.println("子类静态方法");
}
}
2.虚方法:需要在运行期间才能确定最终执行的是哪个方法
简单来说,虚方法就是指可以被重写的方法,除了虚方法之外的都是非虚方法。
虚方法: xx.方法
(1)静态分派
编译时看xx.的编译时类型,在编译时类型中去匹配合适的方法。
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
(2)动态绑定
运行时看xx.的运行时类型,在运行时类型中看是否重写了刚刚找到的匹配方法,
如果有重写,就执行重写的代码,否则就执行刚刚在编译时类型中找到的匹配方法的代码
需要格外注意的是:静态分派->动态绑定,是有先后顺序的,即使子类中有参数列表匹配度更高的方法,系统也是不会直接去调用这个方法的,而是先从父类中找参数类型兼容的方法,然后看这个方法在子类中有没有被重写。如果被重写了,那就执行这个被重写的方法;如果没有被重写,那么它依旧执行父类中的这个方法。
/*
*/
public class TestVirtual {
public static void main(String[] args) {
Fu f = new Zi();
f.method();//(1)静态分派,f的编译时类型是Fu,去Fu类中找method()
//(2)动态绑定,f的运行时类型是Zi,去Zi中看是否有对method()进行重写,如果有,就执行重写的
System.out.println("---------------");
BaBa b = new ErZi();
b.method();//(1)静态分派,b的编译时类型是BaBa,去BaBa类中找method()
//(2)动态绑定,b的运行时类型是ErZi,去ErZi中看是否有对method()进行重写,如果有,就执行重写的,如果没有重写,就执行BaBa类中的
}
}
class Fu{
public void method(){
System.out.println("父类的method方法");
}
}
class Zi extends Fu{
public void method(){
System.out.println("子类的method方法");
}
}
class BaBa{
public void method(){
System.out.println("爸爸类的method方法");
}
}
class ErZi extends BaBa{
//不是重写
public void method(int a){
System.out.println("儿子类的method方法");
}
}