Java学习记录:2022年1月13日(其二)

Java学习记录:2022年1月13日(其二)

摘要:本篇笔记主要记录了在设计类时的一些注意事项,类加载时类中各个部分的加载顺序以及继承和多态的知识。

1.域初始化

​ 域初始化就是一个对象的属性的初始化,我们在13日的笔记(其一)中得知了属性的声明以及初始化的区别,当一个属性被人为的赋值时,才被系统认定为初始化,因此一个对象类型变量的初始化不代表它里边的属性被初始化了。如下:

class Cat{
	private int a;
    private int b;
    private int c;
}

​ 这个类被声明了三个域,分别为a、b、c,他们在被声明的时候没有被赋值,实际上在声明类的时候赋值并不是一个特别好的设计方式,因为类本身就是作为一个抽象概念存在的,它里边的属性列表只是标定了这一类的对象都有什么属性,而非是要标定这个类中的属性都具体是什么值,毕竟每一个人类虽然有相同的属性,但是都具备足以区分彼此的属性值。

​ 因此当我们根据这个类实例化一个对象的时候,如下:

Cat cat1 = new Cat();

​ 这个cat1变量确实是初始化了,但是cat1对象的属性:a、b、c没有被初始化,它们尽管有一个基于安全被系统赋予的初值,但是系统认定这个初值除了保障安全以外没有其他意义,认定这个域是没有被初始化过的,因此我们在声明一个对象类型变量后,需要对这个对象的域进行初始化。怎么进行初始化呢?大部分情况下我们使用对象.属性名 = XXX的方式初始化就好了,但在这里不行,因为这个类的属性都是private类型的,都是私有属性,我们无法直接在对象的外部调用它的属性,因此我们在这个时候通常会使用修改器,如果要使用修改器,就必须这样定义这个类:

class Cat{
	private int a;
    private int b;
    private int c;
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}

​ 然后在声明并初始化之后这样使用:

Cat cat1 = new Cat();
cat1.setA(123);
cat1.setB(456);
cat1.setC(789);

​ 这种写法着实有点麻烦,当然不是说使用修改器是一件麻烦事,修改器是一个很有用处的东西,但是使用修改器为一个对象的域进行赋初值,实在是一件不太简明的写法,当一个类的私有属性非常多的时候,使用修改器进行初值赋予会导致代码非常的冗余,且当你漏写一个的时候,系统并不会报错,这时我们选择构造器,使用构造器进行域初始化是一件再好不过的事情了。如下:

class Cat{
	private int a;
    private int b;
    private int c;
    public Cat(int a, int b, int c){
        this.a = a;
        this.b = b;
        this.c = c;
    }
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}

​ 当我们书写了这样一个构造器的时候,在进行类实例化的时候,就不得不这样书写:

Cat cat1 = new Cat(123,456,789);

​ 否则就会报错,这是因为类中自带的构造器的权限强度非常弱,系统认为它只是一个用于占位的结构,而非一个有实际价值的构造器,当虚拟机系统在编译的时候检测到用户书写的构造器之后,就会认定但凡是用户写的,那一定是有用的,而既然是有用的,那用处一定是大于默认构造器的,因为默认构造器是一点用也没有,这时用户书写的构造器就会完全替换默认构造器,默认的无参构造器便完全不能被调用了。因此当我们这样设计构造器的时候,如果使用的是稍微高级一点的IDE,只要属性漏写之后,它一定会标红,我们就能意识到:“奥,少写东西了。”与此同时,使用构造器这样进行赋初值,也是一个非常简单明了的写法,比使用修改器方便了很多。当一个对象在被声明出来的时候就有自身的含义时,最好使用构造器对其进行赋初值,这样对于整个程序的安全性都有很大的意义

2.构造器的详解

​ 之前我们提到过构造器的作用,实际上不是构造对象实体,而是进行一个对象实体的初始化,同时也简单学习过构造器的一些基础知识,比如构造器的特点为:

1.构造器与类同名

2.每个类可以有一个以上的构造器

3.构造器可以有0个、1个或者多个参数

4.构造器没有返回值,自然在定义时也没有返回值类型

5.构造器总是伴随着new一起使用

​ 而对于构造器的一些其他用法,我们并没有深入的探讨过,现在我们对构造器的一些具体用法进行深入探讨,需要注意的是:构造器的所有用法都和上面列出的五条特性相关

1.任何一个类中存在一个默认构造器

​ 在之前的笔记中提到过,默认构造的存在的作用是为了体现Java中类实例化的机制特性而默认存在的,它的存在只是告诉大家:使用Java语言,实例化一个对象,我们可以使用构造器这一机制来初始化一下这个实例。基于这个机制的重要性以及底层设计的简约行,Java规定无论如何必须使用构造器,不使用构造器初始化一下的话就会报错,但有时大家确实用不到构造器,因此在Java语言中设计了一个默认的无参构造器,如果用户不定义自己的构造器时,就需要使用这个构造器占位,保证虚拟机编译时不会检测出错误,这个无参构造器什么也不做,它内部是空的,它只起到一个占位的作用,并且提示着大家:Java类实例化支持使用构造器对对象实力进行初始化

2.用户自定义构造器与默认构造器

​ 构造器本质上是方法的一种,众所周知在Java中支持方法重写,只要方法签名不一样,那方法都可以用,但是:用户在自己写一个构造器后,默认的构造器就不能用了。这是因为默认构造器被系统认定为:仅用于占位的,无其他作用的构造器。当用户自定义一个构造器,那用户肯定是有自己的考量,肯定是有额外的需求,肯定是需要一个有用的构造器,如果不是这样,那用户为何要费力自己写一个呢?这时肯定是默认构造器不满足用户需求,用户才定义的,基于这种思想,Java被设计成:一旦用户自定义一个构造器,默认构造器便自动隐退。因为它什么用也没有,用户自己写的构造器也可以占位,可以让系统不报错,不仅如此,它还有其他的更多的功能,本着代码简约的原则,这个默认构造器就没有存在的必要了,因此当一个类中存在用户自己定义的构造器时,默认构造器自动失效,不再被可被调用。然而规矩是死的人是活的,有些人在自己定义了构造器之后,仍然想使用之前的无参构造器,这时我们虽然不能使用默认的无参构造器,但是可以自己写一个无参构造器,如下:

class Cat{
	private int a;
    private int b;
    private int c;
    public Cat(int a, int b, int c){
        this.a = a;
        this.b = b;
        this.c = c;
    }
    public Cat(){
        
    }//再自己定义一个无参构造器就行
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}
3.类中可以有多个构造器

​ 这实际上是基于构造器的的本质实际上是方法,它是一种特殊的方法。在Java的类中,方法名不是区分方法标识,方法签名才是,方法签名包含:方法名,形参列表。因此只要形参列表不同,即使构造器们的方法名都必须是类名,也无所谓,可以同时存在多个。

4.构造器可以有任意的参数

​ 构造器的参数列表没有严格的限制,通常来说构造器多被用于进行对象属性的赋初值,不同的参数列表是针对不同情况的赋初值的,如果有需求的话,自己定义一个无参构造器也是可以的,这个并没有严格的定义,复合设计结构和我们的需求即可。

5.构造器的嵌套调用

​ 在构造器中我们可以调用其他构造器,因为构造器实际上就是一系列的逻辑代码,是一系列的操作,能在一个方法中调用另一个方法实际上是很正常的事情,关于构造器的调用有两点注意:只有构造器能调用构造器,其他方法不能调用构造器;在构造器调用构造器时,必须位于第一行。如下图所示:

​ 我们在构造器内部调用一个构造器时,调用方式是直接使用关键词this进行调用,而非使用方法名,否则会报错,上图是一个正确的调用,现在我们来看看各种不正确的调用方式:

HuQrHU.pngHuQ6N4.pngHuQfjx.pngHulQPJ.png

​ 上面图中这三种情况基本上代表了所有的错误情况,总而言之,关于构造器内调用构造器的方式,是直接使用this关键字调用,这个实际上就是认为设计导致的,因为this在类中可以表示被实例化的类的某个对象本身,而构造器实际上也是属于一个对象的,因此就使用this直接表示它自己的构造器了,这样简单易懂,可以节省不少代码量。在类的内部,实际上是以this代表自身生成的对象的,使用this的逻辑代码实际上就是在生成对象或者进行操作时对于本身的调用。而构造器实际上也是属于对象的,因此this也被设计成这个对象的构造器的调用方式。这里我们简单记为构造器的调用方式为this(参数列表)即可。

​ 那么构造器为什么必须在构造器中调用,且构造器只能在第一行被进行调用呢?这是因为构造器不是普通的方法,它被加上了限制,构造器被要求必须在这个实例的实体被构造完成之后,地址返回之前进行执行,它完成的是这个对象中一些信息的初始化。也就是说,它必须在一个对象中的各种信息被初始化之前被调用。因此,如果这个构造器不被书写在第一行,在它之前就有可能存在对整个对象中的域有意义的修改,如在前面对一个域进行了赋值,而在进行有意义的赋值之后,再调用其他的构造器,其他的构造器中也难免存在对同一属性进行的赋值操作,这样一来,就会导致之前的赋值被破坏,进而导致最终得到的一个错误赋值。而在其他普通方法中则更不能轻易调用了,一来是普通方法在被调用的时候,这个对象已经创建好了,这时系统中已经不允许这个对象再使用构造器了,其次,即使系统允许,构造器在这里也会破坏这个对象的数据,导致数据错误,因此基于安全考虑,这种情况也不被允许。

总体上构造器必须在第一行是基于安全考虑,防止它破坏已有的有意义的属性赋值,至于在上面第四张图中,为什么在调用构造器的前几行没有对属性的赋值,它也会报错呢?这是因为虚拟机没有那么智能,它并不能完全确定某一次赋值行为是都是为属性赋值,因此基于绝对的安全性考虑,构造器在构造器中的调用被规定在了第一行。只要在第一行,不管怎样,它都不会破坏我们自己书写的已有的赋值了,这是绝对安全的。因此,构造器在构造器中的调用必须被书写在第一行

在构造器中调用构造器时,之所以必须在第一行调用,是因为是考虑到如果前面的行存在关于属性的赋值行为,构造器的调用可能会破坏已经赋值好的数据。是基于安全考虑的。

3.类的初始化(面试重点)

​ 类在刚刚进入整个虚拟机中准备被进行使用和编译的时候,是需要进行初始化的,这个过程我们称之为类初始化,类初始化和类实例化不是一码事,类初始化是类在进入虚拟机是被加载到方法区中时的这个过程,我们也称这个过程为类加载的过程。类加载过程在它第一次被实例化为一个对象的时候执行

​ 在类初始化的整个过程中,类中的所有部分都会被加载,有些部分还会执行,接下来我们介绍类在初始化的时候各个部分被执行的过程。首先我们需要知道的是类中一般都有哪些组成部分以及哪几类部分。

​ 在类中,主要分为:静态块,普通块,以及一个特殊的部分:构造方法。在一个类中,静态块永远是优先被执行的,普通块其次,而在整个过程中,构造方法是最后被执行的,接下来我们使用代码了解这一过程:

package www;

public class Car{
	private static String name;
	private String gender;
	public Car(){
		
		System.out.println("我是一个构造方法");
	}
	{
		int a = 10;
		System.out.println("我是一个普通块");
	}
	
	static {
		int a = 10;
		System.out.println("我是一个静态块");
	}
}

​ 这个代码的输出过程为:

我是一个静态块
我是一个普通块
我是一个构造方法

​ 也就是说,在一个类中,当这个类被加载的时候,静态块是先被执行的,普通块是在静态块之后被执行的,而构造方法是最后被执行的。我们知道,类加载的过程实际上是这个类被加载到方法区的过程,在这个过程中,类中的静态区域会被优先赋予一次内存,它们的内存会被赋予到静态资源区上,这个过程是优先执行的,因此静态的区域是最先被执行的。而普通块的执行,则是在为这个对象记性地址分配的时候进行的,这个步骤是在堆内存上记性的,它是晚于在方法区被分配内存的,因此它自然地也比静态块执行的时间晚。那构造方法为什么会在最后被执行呢?这是因为构造方法是在整个内存空间被分配好之后才被执行的,它是最后的善后人员,因此它是最后被执行的。

​ 所以在一个类进行第一次对象声明的时候,内部的区域执行的顺序是:静态块(包括静态域和静态块以及静态方法,他们之间的先后加载由书写的先后确定,其中静态方法不会被执行,它作为一个方法仅在被调用的时候执行,被执行的是静态块,但是他们是同时间被加载到内存上的)--------->普通块(就是不被static修饰的各种域,块以及方法,他们实际上是在被分配到堆空间上时执行的)--------->构造器(构造器是在一个对象实体创建好后被执行的,也就是说它在普通块的空间被分配好之后执行,这就是它最后被执行的原因。)

​ 当这个类存在继承其他类的情况时,如下:

package com.ysmw;

public class Car {
    public int speed;
    public int number;
    public int type;
    public Car(){
        System.out.println("我是一个Car类的构造方法");
    }

    {
        System.out.println("我是一个Car类的普通块");
    }
    static{
        System.out.println("我是一个Car类的静态块");
    }
}
package com.ysmw;

public class Truck extends Car {
    public Truck(){
        System.out.println("我是一个Truck类的构造方法");
    }

    {
        System.out.println("我是一个Truck类的普通块");
    }
    static{
        System.out.println("我是一个Truck类的静态块");
    }
}

运行结果为:

我是一个Car类的静态块
我是一个Truck类的静态块
我是一个Car类的普通块
我是一个Car类的构造方法
我是一个Truck类的普通块
我是一个Truck类的构造方法

​ 也就是说,当一个类继承了其他类作为父类的时候,在它被实例化的过程中,类加载的步骤为:父类中的静态块--->子类中的静态块---->父类中的普通块----->父类中的构造方法---->子类中的普通块----->子类中的构造方法

​ 我们发现:当存在继承关系的时候,静态块的执行肯定是优先的第一顺位,也就是说,子类中的块也好,父类中的块也好,肯定是静态区域的优先被执行;第二顺位是父类优先,从上面的运行结果来看,尽管父类和子类中的静态块都要优先于其他部分执行,父类的静态块仍然优先执行。

​ 第二顺位是父类优先原则还导致了在静态块优先运行完之后,就是父类中的其他部分优先执行,即使是父类中的构造器,也要优先于子类中的构造器,在父类的其他部分按照规律运行完毕之后,才轮到子类中的其他块。

​ 我们记为:静态优先为第一顺位,父类优先为第二顺位,普通块较构造器的执行并不体现在第二顺位中,子类除静态块以外的部分,需要等父类的除静态块以外的部分全部运行完之后才会运行。

​ 实际上这是因为Java运行时在进行类加载以及类实例化时的机制:首先检测到了Truck类使用了new语句,这时会检索Truck的路径找到这个类,如果这个类没有被加载过,那么这时就要对这个类进行方法区上的加载,当对这个类的信息进行扫描时,首先发现了这个类里边继承了Car类,这时系统便会顺着路径先加载Truck类继承的Car类,然后就开始对Car进行加载行为,对Car进行加载的时候发现Car中存在静态标识的块,这时就会为这个块分配静态资源区上的空间,被分配空间后的逻辑代码就会被执行,因此在这个时候,Car类中的静态块首先被执行,之后Car类被加载好之后,运行时系统会退回Truck类的操作,继续加载Truck类,这时运行时发现Truck也有static修饰的语句块,这时运行时也会为这些静态语句块分配静态资源区的内存,因此这时这些语句也会被执行。在加载完子类之后,类加载过程就宣告结束了,之后会进行对象实例构造的过程,在对象实例构造的过程中,是要为这个类进行实例化,这时对象中的其他部分会被分配堆空间上的内存,因此普通代码块的创建执行在这个时候才会发生,而即使是在创建对象时,也是先创建一个父类对象,然后才创建子类对象,因此这时会先执行父类中的其他块区,然后执行完父类中的其他块区后,再执行父类相应的构造函数,而这个构造函数会将父类对象的地址返回到子类实例中的类信息中,这个信息在子类中通过super可以查询到,也就是说在父类对象完全创建完毕之后,才会继续创建子类对象,之后子类对象会执行自己的普通区块,并在对象完成内存分配之后,执行构造器返回自己对象实例的地址。

​ 因此我们知道了:静态部分是在类的加载中进行,其他部分都是在类的初始化中进行,而类的加载仅在类第一次被实例化的时候进行一次,因此当我们重复的new对象的时候,会发现静态块只执行了一次之后便不再执行了,且静态部分永远是在最前边,因为静态部分的执行和其他部分的执行完全不是在同一个内存,不是在同一个过程中的。

4.在构造方法中调用超类的构造方法

​ 超类实际上就是父类,在子类中,使用super调用父类对象中的东西,同时,在构造器中可以使用super()来调用父类的构造方法,同时,当我们在构造器中调用父类的构造方法的时候,父类的构造方法也要出现在第一行,如:

package com.ysmw;

public class Truck extends Car {
    public Truck(){
        super();
        System.out.println("我是一个Truck类的构造方法");
    }

    {
        System.out.println("我是一个Truck类的普通块");
    }
    static{
        System.out.println("我是一个Truck类的静态块");
    }
}

​ 否则就会报错:

​ 关于这里的原理我并不是特别清楚,在之后的笔记中会有相关的解答,目前我们先记住:在子类构造器中调用父类构造器时,使用super()进行调用,它和this一样必须位于第一行,且它和this()不能共存

5.静态导入

​ 在Java中有着普通的导入方式import,还存在另外一种导入方式:静态导入。静态导入的书写方式为import static,如下:

import static java.util.*;

​ 静态导入和普通导入的区别在于,静态导入是在整个程序运行之初,就将导入目标直接加载进内存中,虚拟机使用的时候,直接从内存中获取类信息并进行类加载,而非从硬盘中请求,这样一来会有效的加速程序运行速度,但是会导致内存高占用的情况。

6.继承

1.关于在子类构造器中调用父类构造器的问题

​ 在Java中存在一种概念:继承,继承指的是一个类可以像继承基因一样继承另外一个类的所有公共方法,公共属性。基本上除去private,属性都可以继承,没有修饰符修饰的属性,当跨包继承时不会继承来。关于各种修饰符的权限,这里有一个不错的文章。Java中不存在直接的多继承,一个类只能直接继承一个父类。继承方式是使用extends关键词来继承,如下:

package com.ysmw;

public class Truck extends Car {
    public Truck(){

        System.out.println("我是一个Truck类的构造方法");
    }

    {
        System.out.println("我是一个Truck类的普通块");
    }
    static{
        System.out.println("我是一个Truck类的静态块");
    }
}

​ 在继承关系中,父类也被成为超类,子类也被称为孩子类,派生类。根据修饰符的访问权限,子类了可以继承到当下一切可以访问到的方法,子类中可以书写这个方法的同名方法进行覆写。覆写指的是子类中书写了一个和父类中同名的方法之后,这个方法就会覆盖父类中的方法,作为子类独特的方法存在,我们使用子类对象直接调用到的这个名字的方法的方法逻辑,就是在子类中书写的,完成的功能也是子类方法所对应的功能。当然我们可以通过super这个关键字对父类对象进行访问,使用这个关键字我们就可以在子类中调用父类,进而调用父类的方法影响父类内部的属性值,在构造出一个新的子类对象时,会先构造一个没有名字的父类对象,这个父类对象不能直接被调用,使用在子类中使用super关键字可以进行直接的调用。

​ 在上面我们曾经提到了一个知识点,那就是在子类构造器中调用父类构造器时,父类构造器要在第一行,这是因为如果不在第一行,子类构造器中的代码会优先于父类构造器执行,这是不符合初始化顺序的,因此我们必须将父类构造器放置到第一行,让它在子类构造器执行之前进行执行。这里实际上涉及一个底层知识,父类的构造器即使被放置在子类构造器中,也会优先于子类中的普通块以及构造器执行,但是当它不是位于第一行时,这个机制就被破坏了,因为在它之前有子类构造器的代码,子类构造器就已经开始执行了,这样就会直接违背初始化顺序,因此会报错。我们可以记为:父类构造器必须优先于子类构造器执行,因此在子类构造器中调用父类构造器的时候,必须在子类构造器代码执行之前进行调用。这里是一个面试点,需要我们注意。

​ 更加需要注意的是,当父类的构造器需要传参的时候,在子类中必须重写构造器并在子类构造器中调用这个含参的父类构造器,这样才能为父类对象初始化的时候传参,这也是一个面试点,需要我们注意。这里的原理实际上是因为如果子类不在构造器的第一行书写父类构造器,编译器也会在这里加上一个无参构造器,实际上父类的构造器就是在这里的,在程序执行的时候系统会检测子类构造器中的这个位置,并将检测到的父类构造器调用进行执行,也就是说这里无论如何会被加上一个父类构造器的调用,当这里不写this()或者super()时,在编译的时候系统也会在这里加上父类的默认构造器,因此当父类构造器需要传参的时候,这里必须写父类构造器中的含参构造器。关于这个问题,我进行了更加详细的解读,详情请点击链接

2.关于继承的详解

​ 一个类只能有一个父类,但是可以有很多孩子类。每次继承,只能在extends后边写一个继承类名。或者说,对于向上的继承,类不能同时继承多个类。继承是一个树状的结构,因为一个类只能直接继承一个父类,不能同时继承多个父类,不能够多继承,但是实际上可以间接的继承多个类。一个类也可以被多个类继承。由于这种继承树的存在,一个类可能有很多子孙类,有间接的有直接的,越往下,这些类加入的自己的东西就越多,可以说越往下的越新。因此基于木桶原则,Java中引出了多态的概念。

​ 多态就是多种形态的意思,也就是说父类可以表现为任一子类的形态,或者说父类类型的句柄可以承受子类类型的对象实体值。为什么说是木桶原则呢,因为父类中没有的属性,子类中可能有,因此一个子类的创建好的实例可以被塞给一个父类的句柄,因为根据父类的类信息,在这个实体中可以找到父类有的所有东西,因此系统不会报错,简而言之就是只要父类有的子类都有,因此父类没有的部分可以被屏蔽,被屏蔽之后,这个子类就会被伪装成这个父类的样子,这就叫多态。而子类的句柄不能承接父类的,因为子类中有一些东西父类是没有的,因此当我们把一个父类的实体赋予给一个子类的句柄,使用子类句柄进行操作时就会出现各种空指针错误,因此这种反过来的赋值是行不通的,多态只能指:将子类的值赋予给父类的值。

​ 实际上就是向上兼容的原理,父类好比是一个能装1升水的小木桶,子类是一个能装5升水的大木桶。子类可以通过锯掉上边的部分变为父类,但父类却不能通过增加木头变成子类,这便是多态的原理,子类对象可以通过父类句柄的访问规则屏蔽掉多出来的字段,而父类对象中没有的东西,使用子类的访问规则就会出现空指针异常。因此父类句柄 = 子类对象,将一个子类对象转变为父类类型的行为被称为多态。因此,使用多态时,超类只能调用自己确实声明过的方法,子类自己创新出来的新方法超类用不了。

​ 当我们将一个子类对象变为父类对象的时候,可以使用强制类型转化再转化回来,或者转化成自己的其他父类,这个强制类型转化的过程实际上就是恢复被屏蔽掉的部分的行为,当然这个过程也充满了各种复杂情况,如下:

声明一个类A
B继承了A,C继承了B,D继承了C
E继承了A,F继承了E

​ 这样一来就有如图所示的继承树:

​ 这时我们就可以做如下的操作:

A a = new D();//将D类型的实例存放进A类型的变量中
a = (D)a;//将a句柄强制转化为D类型

​ 当我们对a进行强制转化时,还可以转化为自己的其他父类,比如C类,B类,但是注意E类和F类转化不了,因为E类和F类不是D类同一个路径上的父亲,可以说不是直系亲属,这会导致E和F中可能存在D中不存在的字段,因此不能将D强制转化为E和F在运行的时候会报错,可能会出现严重的空指针错误,难受的是IDE不会喂为你自动报错,它检测不出强制类型转化时的问题,因此我们在使用强制类型转化的时候,需要使用到一个方法:instanceof,这个方法用于判断前边的对象是否是后边的子孙后代类的对象。使用方法为:

d.instanceof(A);//检测变量d的类型是否为A类型以及是否为A的子孙类型,在进行强制转化之前先使用这个方法进行检测,更加安全。

​ 如上为13日的基本内容,下面我附上笔记原文。

7.笔记原文

​ 需要注意的是笔记原文仅供参考,且笔记原文中存在一些错误以及之前不懂得知识点,在上边的笔记中已经对里边的疑问进行了全面的解答,因此在查看笔记原文时如果发现有地方是错的或者说进行了标注,说暂时不明白,请直接忽略。

	整个类组成
	类的属性也叫域
	构造方法没有返回类型,与类同名
	方法同名,入参不同,系统就可以区分,这叫方法的重载
	在new对象的时候,new后边调用的时候就会调用这个构造方法,对于有参的构造方法,并不会完全覆盖无参的构造方法,都可以用
	类中还有一个东西叫注解,也是java类信息的一部分
	识别方法只看方法名和入参,不看返回类型,因此方法名和入参一样,返回类型不一样,仍然会报错,注意
	只要方法名和入参一样,就会认为是同一个方法
	this表示的是隐含当前对象的意思,也就是在对象里边使用自己的域时,为了和外部参数的同名变量区分,通常使用this来表示是自己的变量的意思。我们可以理解为,当在一个类的内部对自己使用某方法或者调用自己的域时,用this
	在每一个方法中,this表示隐式参数,也就是说在每一个方法中,表示自己的隐式参数是this
	final为什么不可修饰引用类型?final只对被他修饰的句柄阻止赋值,它不允许整体赋值,但是可以对里边进行修改,也就是说对于堆地址它并管不到,会导致不可控。final对于引用类型是防止改变指向,防御不了引用类型在本地址上修改,但是它可以修饰字符串,因为字符串不可变,在本地址上改不了,因此可以修饰字符串。final修饰的引用类型不能改变句柄的指向,但是放置不了指向地址上的修改。
	静态的常量在一开始必须赋值,在域中的的常量不一定赋值,但是在类加载的时候必须执行。
	final修饰的变量要求它存在的时候就要有值,静态的上来就要赋值,因为静态的在进入内存的时候就已经存在了。
	代码块在类加载的时候会执行,什么是类加载?就是加载进方法区的时候,原来如此,那么我上边理解错了,final修饰的在类加载的时候赋值就行了。
	如果涉及到公共的,static修饰的变量,它会向全程序公开,因此这时我们会用final将它修饰成常量,让其全程序公开且不可变
	这样的常量可以直接用类进行调用,只要在一个包下,就可以用类加常量名进行调用
	跨包调用一般来说就不能直接访问了,public是包内访问,要想跨包,就得先导包。
	静态方法就是无需使用实例,直接使用类,就可以直接调用,因为它随着类已经加载进内存里了,它已经存在了
    设计模式需要大量的代码经验,常见的是23种,是重要的面试点!
	如果传的是堆中地址,传的是值叫值传递,传的是栈中地址叫引用传递,栈地址叫引用地址,也就是说基本类型传的是栈地址,是引用传递
	我们可以简单记住引用传递传的是栈中的地址,然后将地址中的信息拷贝进入参
	值传递是传递值,引用传递是传递真正的地址
	默认域初始化就是让变量默认的有初值
	上边我想错了,对于引用传递我是学习过的,这一点绝对没有错,不过确实可以单纯的从位置来看,我明白了,用&取地址是一个原理,关键在于我们是得到了源数据还是一份拷贝,引用传递就是拿地址,拷贝的不是值而是栈上的地址

	显式的域初始化,就是在写类的时候给出初值,声明变量的时候给出初值,这样一来,我们只要实例化一个对象,其初值都是那样的。
	命名其实还是要按照相应的命名规则来命名,不要不规范,一定要规范一些,该大写大写该小写小写,该如何驼峰就如何驼峰,总之要符合命名规范。以后还会更加详细的讲命名规范
	我们可以在自身调用自身的构造方法,可以用this来调用,必须在第一行
	在自身的构造方法中可以调用自身的构造方法,这样可以再构造一个类,或者灵活的生成需要的类和类之间的连接
	注意只能在构造方法中调用,且必须在第一行
	4.7.6是面试重点
	类初始化(生成对象)顺序是什么
	或者对象初始化顺序
	普通方法块优先于构造方法,静态方法块优先于普通方法块,静态的块只执行一次,它在第一个对象处执行
	非静态的属性和非静态块属于同一优先级,谁在上边谁先执行,二者是同一个优先级。
	所有的静态区域只由第一个对象触发,并且只执行一次,包括静态块,静态域。静态只能由第一个对象触发
	即使我们new一个静态对象,这里使用一次构造方法,它是第二个类,它也不会触发任何静态了,只能是它执行完之后,第一个构造方法接着来触发后边的静态类型。也就是说静态类型只能由第一个构造方法触发
	new Person()表示生成一个对象,this()仅仅表示调用方法,二者还是有差别的,生成对象是可以在任意位置使用的,单纯调用方法的话是有严格限制的。也就是说加上new之后就不是一码事了,加上new之后就会返回一个对象。我们不能使用new this()的方式构建对象,只能是使用new 类名构造器的方式进行对象构造
	一个类如果继承了一个父类,那么在这个类初始化的时候,一定是父类先初始化,这个类才能初始化,父类中的东西先进行初始化,但是我发现了一个问题,永远是静态的东西优先级更高,也就是说在父类中有静态有非静态,子类中有静态有非静态时,父类的静态先,然后是子类中的静态,然后是父类中的非静态,最后再试子类中的非静态
	同一类重复初始化时,才会体现出静态块只加载一次的现象,当不同的类分别初始化,他们的静态块都会执行
	首先是静态优先,然后是其他其次,对于子类父类,父类限制性,子类其次,也就是说先是父类的静态先执行,子类静态第二,然后是父类全部非静态优先,最后是子类
	这里是面试大重点!一定要记住!
	这里的顺序一会再整理一遍,详细的记住!

	在Java中实例不用之后,会自动析构
	Java类一定要存在于某个包下,包实际上就是一个文件夹
	静态导入是一开始就放入内存,这样运行起来会非常快
	包的命名很多都是域名倒过来开头
	包上边的一个点代表一级目录,如com.cloud相当于com目录下的cloud目录
	继承:子类可以继承父类一切的公共方法,包括其主方法,子类会继承所有的内容,包括私有属性也会继承,好像只有保护的不被继承,这时会新建一个没有名字的父类实例用于存储父类信息,子类使用的信息都来自于这里,注意!
	子类不能继承父类的私有属性,但是如果子类中公有的方法影响到了父类私有属性,那么私有属性是能够被子类使用的。
	父类也叫超类,基类;子类也叫派生类,孩子类
	方法的覆盖,不管父类中的方法是什么,只要在子类中书写了同名的方法,其就会覆盖住父类中的方法,当调用子类中的这个方法时,调用的是子类中的新方法,这就叫覆盖。
	重写就是覆盖,覆盖就是重写
	在子类中使用父类构造器时,父类构造器必须写在第一行,因为在初始化时,父类构造器必须先初始化,因此必须在第一行。、
	自己调用自己的构造器,必须在第一行,而调用父类的也必须在第个行
	如果父类的构造器需要传参,那么子类在自己的构造方法中必须要调用父类的构造方法并进行传参,斗则会报错。这里也是一个面试点,注意!
    需要尤为注意的就是这一点,在父类的构造器需要传参时,子类的构造器一定要重写一个构造器,在里边调用父类的构造器并传参

	一个类只能有一个父类,但是可以有很多孩子类。每次继承,只能在extends后边写一个。或者说,对于向上的继承,类不能同时继承多个类,继承是一个树状的结构。一个类只能直接继承一个父类,不能同时继承多个父类,不能够多继承,但是实际上可以间接的继承多个类。
	多态就是多种形态的意思,父类可以表现为子类的任何一种形态,或者说父类可以承受其子类的形态
	可以用父类变量 = 子类对象,父类可以接受子类实例的赋值,进而展现出子类的样子,这叫多态
	但是使用多态时,超类只能调用自己确实声明过的方法,子类自己创新出来的新方法超类用不了


	这种方法最常用,祖宗类作为参数,他能够接受所有的子孙类。和父类是直系亲属的子类都可以作为参数传进去。
	总之就是父类的变量可以承担子类,但是子类的方法父类不见得能用,这里只能用父类有的方法。超类对象是不是只能调用子类重写的方法,子类新加的方法不能调用
	阻止继承的方法是使用final前缀,被final修饰的类无法被继承,被final修饰的方法无法被重写
	强制类型转换只要满足,祖宗类的句柄 = 子孙后代类的对象的地址
	也就是说只能向上强制转换,多态是向下,强制类型转换是反过来向上
	被多态化为子类的对象可以被强制类型转化回父类。
	instanceof判断前边的对象是否是后边的子孙后代类的对象
	Object类是所有类的父类
	这里不太明了,再看!
	抽象是提取共性,这个叫做抽象,也就是只具备重要特征
	在类中给某属性写死值没有意义

	子类需要使用父类中的方法,但是具体怎么使用不可控,这时适合使用抽象类。抽象类只定义有,不能替子类实现功能。抽象类一般都规定子类有什么功能,但是规定不了怎么实现。
	抽象类是做限定的,具体实现的靠子类
	抽象类侧重于抽象,其子类负责具体实现
	继承抽象类之后一定要实现抽象类的方法,否则会报错

	父类变量可以承接子类变量,这个行为展现的是多态的性质,同时这个行为被称为向上转化。
	在此为前提下的情况下的父类变量可以再次转换会子类变量,这时就需要一个强制转化,


	在整个过程中,只允许出现父类变量 = 父类地址或者父类变量 = 子类地址的状况
	不能出现子类变量  = 父类地址的情况
	句柄类型一定要是父类,句柄类型一定要是真实地址类型的父类才行,句柄类型比较容易看出来,但是真实地址的类型不见得和句柄一致,因为多态
	如 C继承B,B继承A
	A a = new C()
	a句柄中实际上指的是一个C类型的地址
	然后就可以有B b = new B()
	然后有 b = (b)a是这样的

	在整个过程中,只允许出现父类类型句柄 = 父类类型值地址或者父类类型句柄 = 子类类型值地址的状况,不能出现子类类型句柄  = 父类类型值地址的情况因为多态,句柄类型不见得和指向的地址的值的类型一致,如C继承B,B继承A时,有A a = new C()
	a句柄中实际上指的是一个C类型的地址,当存在B b = new B()时可以有:b = (b)a因为b实际上是a句柄指向的地址的值类型的父类
	正因为存在种纷繁复杂的情况,我们要用intenceof语句进行真相判断
posted @ 2022-03-25 11:12  云杉木屋  阅读(26)  评论(0编辑  收藏  举报