文章目录

  • 一、类、方法、属性的详解
  • 1.1、类的定义:
  • 1.2、四种访问权限修饰符的区别
  • 1.3、类的定义方式
  • 二、构造方法(构造器 constructor)
  • 1、构造方法基础用法
  • 2、构造器的4个要点:
  • 3、构造方法的重载
  • 5、引用的特点
  • 6、this关键字
  • 6.1、就近原则
  • 三、Java中栈(堆栈)与堆的数据存储方式
  • 四、Static(深入理解static关键字)
  • 4.1、static存在的主要意义
  • 4.2、static的独特之处
  • 4.3、静态变量和实例变量的概念
  • 4.4、静态变量和实例变量区别(掌握)
  • 4.5、访问静态变量的两种方式
  • 4.6、static静态方法
  • 4.7、代码块的分类
  • 4.8、static变量与普通变量区别
  • 4.9、static注意事项
  • 5.0、final与static的藕断丝连
  • 6.0、static静态代码块以及各代码块之间的执行顺序
  • 6.1、静态代码块(也叫静态块、静态初始化块)
  • 6.2、构造代码块
  • 6.3、代码块(又叫普通代码块、初始化块)
  • 五、final(深入理解final关键字)
  • 5.1、修饰类
  • 5.2、修饰方法
  • 5.3、修饰变量
  • 5.4、final修饰变量(成员变量、局部变量)
  • 5.4.1、final修饰成员变量
  • 5.4.2、final修饰局部变量
  • 5.5、final变量和普通变量的区别
  • 5.6、final与static的藕断丝连
  • 5.7、关于final修饰参数的争议
  • 六、封装、继承、多态的详解
  • 6.1、封装
  • 封装:就是将重复的代码封装起来,或者用private修饰符加以限定,用get/set方法来获取
  • 6.2、继承
  • 6.2.1、继承的特点
  • 6.2.2、继承的语法格式
  • 6.2.3、重写父类的方法
  • 6.2.3.1、方法的重写要遵循两同两小一大规则
  • 6.2.3.2、方法重写的注意点
  • 6.2.4、super关键字的学习
  • 6.2.5、super调用父类构造器
  • 6.2.6、抽象类
  • 6.2.6.1、抽象类和类的相同点
  • 6.2.6.2、抽象方法
  • 6.2.6.3、抽象类和类的不同点
  • 6.2.6.4、掌握抽象类
  • 6.2.7、接口
  • 6.2.7.1、接口的特性
  • 6.2.7.2、接口必知
  • 6.2.7.3、抽象类和接口本质区别
  • 6.3、多态
  • 6.3.1、多态的定义:
  • 6.3.2、多态存在的三个必要条件
  • 6.3.3、实现多态的技术
  • 6.3.4、动态绑定和静态绑定


一、类、方法、属性的详解

1.1、类的定义:

属性(field成员变量)

  1. 属性用于定义该类或类对象包含的数据或者说静态特征,属性作用范围是整个类体。
  2. 在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化

成员变量的默认值:

数据类型

默认值

整形

0

浮点型

0.0

字符型

‘\u0000’

布尔型

false

所有引用类型

null

属性定义格式:

[修饰符] 属性类型 属性名 = [默认值];

1.2、四种访问权限修饰符的区别
  • private(私有权限)

private可以修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类,不考虑内部类),被private修饰的成员,只能在定义它们的类中使用,在其他类中不能调用。

  • default(默认权限)

类,数据成员,构造方法,方法成员,都能够使用默认权限,即不写任何关键字。默认权限即同包权限,同包权限的元素只能在定义它们的类中,以及同包的类中被调用。

  • protected(受保护权限)

protected可以修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类,不考虑内部类)。被protected修饰的成员,能在定义它们的类中,同包的类中被调用。
如果有不同包的类想调用它们,那么这个类必须是定义它们的类的子类。

  • public(公共权限)

public可以修饰类,数据成员,构造方法,方法成员。被public修饰的成员,可以在任何一个类中被调用,不管同包或不同包,是权限最大的一个修饰符。

注意:

1、并不是每个修饰符都可以修饰类(指外部类),只有public和default可以。

2、所有修饰符都可以修饰数据成员,方法成员,构造方法。

3、为了代码安全起见,修饰符不要尽量使用权限大的,而是适用即可。比如,数据成员,如果没有特殊需要,尽可能用private.

4、修饰符修饰的是“被访问”的权限。

public

protected

default

private

同一个类





同一个包




不同包,非子类


不同包,子类



方法

方法的定义:方法用于定义该类或该类实例的行为特征和功能实现。

方法的定义格式:

[修饰符] 方法返回值类型 方法名(形参列表){
 	//语句体   
}
1.3、类的定义方式
/**
 * 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致
 */
public class Teacher {

}

/**
 * 一个Java文件可以同时定义多个class
 */
class Student{

}

上面的类定义好后,没有任何的其他信息,就跟我们拿到一张张图纸,但是纸上没有任何信息,这是一个空类,没有任何实际意义。所以,我们需要定义类的具体信息。对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个。

接下来,定义一个具有具体信息教师类,如下所示:

package com.bjsxt.demo;

/**
 * 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致
 */
public class Teacher {

    private int age;
    private String sex;
    private String name;

    public Teacher() {
    }

    public Teacher(int age, String sex, String name) {
        this.age = age;
        this.sex = sex;
         = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
         = name;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "age=" + age +
                ", sex='" + sex + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

二、构造方法(构造器 constructor)

1、构造方法基础用法

构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。

声明格式:

[修饰符] 类名 (形参列表){
	//语句体
}
2、构造器的4个要点:

1、构造器通过new关键字调用!!

2、构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。

3、如果我们没有定义构造器,则编译器会自动定义一个无参的构造方法。如果已定义则编译器不会自动添加!

4、构造器的方法名必须和类名一致!

2.1、对象的创建完全是由构造方法实现的吗?

答:不完全是,对象并不是完全由构造器负责创建的。创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量初始化为0或空
  2. 执行属性值的显示初始化
  3. 执行构造方法
  4. 返回对象的地址给相关的变量
3、构造方法的重载

构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。

请看如下案例:

package com.bjsxt.demo;

public class User {
    private int id;
    private String name;
    private String pwd;

    public User() {
    }

    public User(int id, String name) {
        this.id = id;
         = name;
    }

    public User(int id, String name, String pwd) {
        this.id = id;
         = name;
        this.pwd = pwd;
    }

    public static void main(String[] args) {
        User u1 = new User();
        User u2 = new User(101, "高小七");
        User u3 = new User(100, "高淇", "123456");
    }
}

新手雷区
如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。如:
this.id 表示属性id;id表示形参id

同一个类中,在构造方法中调用另一个重载的构造器,使用this关键字进行调用。如下所示:

public class Demo {

    String name;
    int age;
    String sex;

    public Demo() {

        this("张三", "男");
    }

    public Demo(String name, String sex) {

        this("张三", "男", 18);
    }

    public Demo(String name, String sex, int age) {

    }

}
5、引用的特点
  • 同一个时刻一个引用只能指向一个对象
  • 一个对象可以被多个引用所指向,其中一个发生对其变化,该对象的其他引 用也可见该变化
  • java 一切皆为值传递 ,引用则拷贝地址,图解如下:

定义测试类java是神魔意思_构造方法

6、this关键字

this 即”自己”,代表对象本身,谁调用代表谁。在成员方法中或构造器中隐式的传递。作用如下:

1、 this.属性避免属性和形参、局部变量同名,发生就近原则

2、 this([实参列表]): 构造器的首行调用其他构造器。

6.1、就近原则

错误演示:

在程序中,代码遵循就近原则,而我们又要遵循见名知意的约定就会出现以下尴尬情况,自己赋值给自己:

package com.bjsxt.demo;

/**
 * @author Rainbow
 */
public class Student {
    private int age;

    public void setAge(int age) {
        age = age;
    }

    public int getAge() {
        return age;
    }

    public static void main(String[] args) {
        Student s = new Student();
        s.setAge(18);
        System.out.println(s.getAge());
    }
}

图解:

定义测试类java是神魔意思_构造器_02

正确演示:this关键字

为了避免这种情况的发生,Java语言提供了 this 关键字来区分类的变量(成员变量)和传入参数变量(局部变量)的区别。

this 表示当前正在运行的对象

package com.bjsxt.demo;

/**
 * @author Rainbow
 */
public class Student {
    private int age;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    public static void main(String[] args) {
        Student s = new Student();
        s.setAge(18);
        System.out.println(s.getAge());
    }
}

图解:

定义测试类java是神魔意思_java_03

三、Java中栈(堆栈)与堆的数据存储方式

注意:在jdk1.8以后;方法区没有了变成了元数据区存储在内存中

定义测试类java是神魔意思_构造器_04

  1. 栈:连续;先进后出,存放局部变量
    栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
  2. 堆:不连续;全局变量,堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:新生代、年老代、永久代
    jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
    存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
    gc:Garbage collection 垃圾回收机制
    1、程序员无权调用垃圾回收器。
    2、程序员可以通过System.gc()。通知GC运行,但是JAVA规范并不能保证立刻运行。
    3、finalize方法,是JAVA提供给程序员用来释放对象或资源的方法,但是尽量少用
  3. 方法区(元数据区):方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。
    方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
    方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

四、Static(深入理解static关键字)

4.1、static存在的主要意义
  • static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法
  • static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
  • 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
4.2、static的独特之处
  1. 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享

怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?

  1. 在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
  2. static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
  3. 被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
4.3、静态变量和实例变量的概念

静态变量:

static修饰的成员变量叫做静态变量【也叫做类变量】,静态变量是属于这个类,而不是属于是对象。

实例变量:

没有被static修饰的成员变量叫做实例变量,实例变量是属于这个类的实例对象。

还有一点需要注意的是:static是不允许用来修饰局部变量,不要问我问什么,因为java规定的!

4.4、静态变量和实例变量区别(掌握)

静态变量:

静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。

实例变量:

每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

4.5、访问静态变量的两种方式

我们都知道静态变量是属于这个类,而不是属于是对象,static独立于对象。

但是各位有木有想过:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问【只要访问权限足够允许就行】,不理解没关系,来个代码就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

运行结果如下:

运行结果: 666

我举的这个StaticDemo例子主要目的是说明一下this也是可以访问static的变量的!!!

因此小结一下访问静态变量和静态方法两种方法:

静态变量:

类名.静态变量

对象.静态变量(不推荐)

静态方法:

类名.静态方法

对象.静态方法(不推荐)

4.6、static静态方法

由于对于静态方法来说是不属于任何实例对象的,this指的是当前对象,因为static静态方法不属于任何对象,所以就谈不上this了。构造方法不是静态方法

4.7、代码块的分类

基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块

代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块

继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器

4.8、static变量与普通变量区别

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。

4.9、static注意事项
  • 静态只能访问静态。
  • 非静态既可以访问非静态的,也可以访问静态的。
5.0、final与static的藕断丝连

接下来先看一个案例:

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修饰的  i=" + demo1.i);
        System.out.println("static修饰的 t=" + demo1.t);
        System.out.println("final修饰的  i=" + demo2.i);
        System.out.println("static修饰的 t=" + demo2.t);
      }
}

运行结果如下:

final修饰的 i=0.7282093281367935
static修饰的 t=0.30720545678577604
final修饰的 i=0.8106990945706758
static修饰的 t=0.30720545678577604

static修饰的变量没有发生变化是因为static作用于成员变量只是用来表示保存一份副本,其不会发生变化。怎么理解这个副本呢?其实static修饰的在类加载的时候就加载完成了(初始化),而且只会加载一次也就是说初始化一次,所以不会发生变化!

6.0、static静态代码块以及各代码块之间的执行顺序
6.1、静态代码块(也叫静态块、静态初始化块)

Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次,也就是说这些代码不需要实例化类就能够被调用。一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块,所以静态块常用来执行类属性的初始化!

关于Static静态代码块的五个小结点

1、Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次

2、静态块常用来执行类属性的初始化

3、静态块优先于各种代码块以及构造函数,如果一个类中有多个静态代码块,会按照书写顺序依次执行

4、静态代码块可以定义在类的任何地方中除了方法体中【这里的方法体是任何方法体】

5、静态代码块不能访问普通变量

由于普通方法是通过加载类,然后new出实例化对象,通过对象才能运行这个方法,而静态代码块只需要加载类之后就能运行了。

我们需要明确静态代码块的存在在类加载的时候就自动运行了,而放在不管是普通方法还是静态方法中,都是不能自动运行的。

6.2、构造代码块

我们都知道,没个方法中都可以有很多构造方法,每创建一个对象其构造方法就执行一个,而一个构造方法可以创建N个对象,构造方法就比较“高冷”了,而构造代码块就比较“舔狗”了,只要该类实例了一个对象,构造代码就执行一次,利用每次创建对象的时候都会提前调用一次构造代码块特性,所以它可以做统计创建对象的次数功能。

构造代码块小结

1、构造代码块在创建对象时被调用,每次创建对象都会调用一次

2、构造代码块优先于构造函数执行,同时构造代码块的运行依赖于构造函数

3、构造代码块在类中定义

第二句话的“依赖”可理解为如果不实例化对象(也就是不执行构造方法),构造代码块是不会执行的!

6.3、代码块(又叫普通代码块、初始化块)

1、普通代码块定义在方法体中

2、普通代码块与构造代码块的格式一致都是{}

3、普通代码块与构造代码块唯一能直接看出的区别是构造代码块是在类中定义的,而普通代码块是在方法体中定义的

为以上的知识点总结写一个案例:请看如下代码:

public class Block {

    public Block() {

    }

    {
        System.out.println("我是构造代码块One");
    }

    {
        System.out.println("我是构造代码块Two");
    }

    static {
        System.out.println("我是静态代码块One");
    }

    static {
        System.out.println("我是静态代码块Two");
    }

    public void method() {
        {
            System.out.println("我是普通代码块。。。。");
        }
    }
}

class Demo {
    public static void main(String[] args) {
        Block block = new Block();
        block.method();
        System.out.println("--------------------");
        /**
         * 多创建几个对象,好体现出static静态代码块只执行一次
         */
        Block block2 = new Block();
        block2.method();
        Block block3 = new Block();
        block3.method();
    }
}

五、final(深入理解final关键字)

5.1、修饰类
  • final修饰一个类时,表明这个类不能被继承。
public final class Father {

}

/**
 * 编译报错,显示不能继承被final修饰的类
 */
class Son extends Father{

}

定义测试类java是神魔意思_静态变量_05

5.2、修饰方法
  • final修饰方法,方法不可以重写,但是可以被子类访问 【前提:方法不是 private 类型】
public  class Father {

    public final void speak(){
        System.out.println("粑粑:不,你想拉粑粑");
    }
}

class Son extends Father{
    /**
     * 直接编译失败,被final修饰的方法不能被重写
     */
    public final void speak(){
        System.out.println("我不是人,但你是真的狗!!!");
    }

    public static void main(String[] args) {
        Son son = new Son();
        //访问final修饰的方法
        son.speak();
    }
}

运行结果如下:

粑粑:不,你想拉粑粑

定义测试类java是神魔意思_构造方法_06

5.3、修饰变量

final用得最多的时候就是修饰变量

如果被final修饰的是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;

如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

public class Demo {
    public final int a = 1;

    public void method(){
        a = 2;
        final String str = new String("HelloWord");
        str = new String("HelloWord");
    }
}

定义测试类java是神魔意思_构造方法_07

5.4、final修饰变量(成员变量、局部变量)
5.4.1、final修饰成员变量
  1. final修饰的成员变量必须在定义时或者构造器中进行初始化赋值
public class Demo {
    public int a;
    public final int b ;
    public final int c = 1;

}

定义测试类java是神魔意思_构造方法_08

  1. 如果用final修饰的成员变量的时候不初始化是否可以呢?
    答:可以的,但是前提必须要在构造方法中将成员变量进行初始化,代码如下:
public class Demo {
    public int a;
    public final int b ;
    public final int c = 1;

    public Demo() {
        b = 2;
    }
}

定义测试类java是神魔意思_java_09

  1. final变量一旦被初始化赋值之后,就不能再被赋值了。【注意是成员变量】
5.4.2、final修饰局部变量
  • 只需要保证在使用之前被初始化赋值即可
5.5、final变量和普通变量的区别
  • 当final作用于类的成员变量时(因为局部变量只需要在使用前被初始化赋值即可),成员变量必须在定义或者构造器中被初始化赋值,一旦被赋值,就不可以再更改了。
public static void main(String[] args) {
    String str = "helloWord";

    String a = "hello";
    final String b = "hello";
    String c = "hello" + "Word";

    String d = a + "Word";
    String e = b + "Word";

    System.out.println(str == d);//false
    System.out.println(str == e);//true
    System.out.println(str == c);//true

}

疑问?为啥第一个为false,而第二个为true???

答:这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的 值。而对于变量a的访问却需要在运行时通过链接来进行。

只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:

public static void main(String[] args) {
    String str = "helloWord";

    final String b = method();

    String e = b + "Word";

    System.out.println(str == e);//false

}

public static String method() {
    return "hello";
}

这段代码的输出结果为false。在编译期间不能确定final变量值,不做优化!!!

  • final修饰的引用变量一旦初始化赋值之后指向的对象不可变但该对象的内容可变
public class Demo {
    int a = 1;

}

class Demo2{
    public static void main(String[] args) {
        final Demo demo = new Demo(); //final修饰引用变量
        
        demo = new Demo2();编译失败,说明被final修饰的引用变量一旦初始化赋值之后指向的对象不可变
        
        System.out.println(++demo.a);//输出值为2,说明内容可变
    }

}

运行结果如下:

2

5.6、final与static的藕断丝连
package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修饰的  i=" + demo1.i);
        System.out.println("static修饰的 t=" + demo1.t);
        System.out.println("final修饰的  i=" + demo2.i);
        System.out.println("static修饰的 t=" + demo2.t);
      }
}

运行结果如下:

final修饰的 i=0.7282093281367935
static修饰的 t=0.30720545678577604
final修饰的 i=0.8106990945706758
static修饰的 t=0.30720545678577604

疑问:不是说好final修饰基本数据类型的变量时,则其数值一旦在初始化之后便不能更改咩?为啥子这里final修饰的基本类型值反而不一致,static修饰的基本类型却一致?

答:请注意final修饰基本数据类型的变量时,则其数值一旦在***初始化之后***便不能更改

上面代码中被final修饰的变量是在***运行时才初始化的***,并***没有在编译期就被初始化***!由于值为随机数,运行时被初始化是不确定的一个值,也就是个随机数,仅仅当运行之后被初始化之后他的值才会不变!

5.7、关于final修饰参数的争议

方法中不需要改变作为参数的对象变量时,使用final进行声明,可以防止你无意的修改而影响到调用方法外的变量,想要理解这句话,请看如下代码:

public class Demo {
    public static void main(String[] args) {

        Test test = new Test();
        int a = 1;
        test.method(a);
        int b = 3;
        test.method(b);

    }
}
class Test {
    public void method(final int parameter){

//        parameter ++;//编译不通过
//        parameter = 2;//编译不通过

        System.out.println(parameter);
    }
}

运行结果:

1

3

  • 上面代码中注释的代码编译失败的原因:被final修饰的是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
  • 方法是运行时才初始化的,执行到 int b=4par.method(a)方法以及结束,再到par.method(b)的时候,又重新初始化了,所以打印结果如上。

未理解的可以继续看如下代码:

public class Demo {
    public static void main(String[] args) {

        Test test = new Test();
        int a = 222;
        test.method(a);
        int b = 333;
        test.method(b);

    }
}
class Test {
    public void method(int parameter){

        parameter ++;
        parameter = 2;

        System.out.println(parameter);
    }
}

运行结果如下:

2
2

  • 因为使用了final,所以不会影响到你在外部传递的参数,而不使用final修饰参数,不管你传递啥,都无效

对于下面这个程序,上述的就不怎么管用了(因为此程序中的**参数用到了引用传递**,只是改变了内容,没有改变地址!)

public class Demo {
    public static void main(String[] args) {
        Test test = new Test();
        StringBuffer buffer = new StringBuffer("Hello");
        test.method(buffer);
        System.out.println(buffer.toString());

    }
}

class Test {
    public void method(final StringBuffer buffer) {

        buffer.append("你好呀!!!");

    }

}

运行结果如下:

Hello你好呀!!!

  • 当我们把final去掉再运行,结果同样是"Hello你好呀!!!"。这就是引用传递,且只是改变了内容,没有改变地址!

六、封装、继承、多态的详解

6.1、封装
封装:就是将重复的代码封装起来,或者用private修饰符加以限定,用get/set方法来获取
  • 将能实现某一特定功能的代码封装成一个独立的实体后,各程序员可以在需要的时候调
    用,从而实现了专业的分工,即工作中的分模块、分功能开发
    这里就不再以赘述。。。。
6.2、继承
6.2.1、继承的特点
  • 继承是通过extends关键字来实现
  • 可以直接访问父类非私有的一些属性和方法
  • 继承具有单继承的特点,每个子类只有一个直接父类
6.2.2、继承的语法格式
修饰符 class Son extends Father{
    //类定义的部分
}
  • 从上述的语法格式来看, 定义子类的语法非常简单, 只需要在原来的类定义上增加extends Father即可
  • 子类继承了父类, 也将获得父类的全部成员变量和方法(有疑问???),但是子类不能继承父类的构造器.
  • 父类的东西,子类可以全部继承吗?
    答:当然不是,原因如下:
  1. 权限修饰符的继承问题
  • 被private修饰的成员变量或者方法,是不可以被继承的,因为private修饰的只能在本类中可见,子类是不可见的;
  • 另外父类的成员变量或者方法被protected或public修饰的,子类是可以继承的但是子类的方法的修饰符必须比父类方法的修饰符要大;
  • 被默认修饰符修饰的只能在同包下的子类是可以继承的;

代码如下:Father.java

定义测试类java是神魔意思_构造器_10

代码如下:Son.java

定义测试类java是神魔意思_静态变量_11

  1. 构造器的继承问题
  • 构造器是不会被子类继承的,但子类的对象在初始化时会默认调用父类的无参构造器。
  • 当父类显示写了有参构造器,且没有无参构造器,子类继承父类的时候必须显示的调用父类的有参构造器。
  • 调用的方式可以使用super(参数)来调用且使用super(参数)必须写在子类构造方法的第一行;
  • super()与super(参数);同时只能调用一个

代码如下:Father.java

定义测试类java是神魔意思_构造器_12

代码如下:Son.java

定义测试类java是神魔意思_构造方法_13

  1. static修饰符的继承问题
  • 子类是不会继承父类被static修饰的方法和变量

代码如下:Father.java

定义测试类java是神魔意思_静态变量_14

代码如下:Son.java

定义测试类java是神魔意思_定义测试类java是神魔意思_15

  • Java类只能有一个直接父类,但是它可以有无限多个间接父类。如下所示:
public class Ye {

}

class Fu extends Ye{
    
} 

class Zi extends Fu{
    
}

从上述不难看出,Ye类是Zi类的间接父类。如果定义一个类时并未显示指定这个类的直接父类,则这个类默认继承java.lang.Object类。

因此可以得出,java.lang.Object类是所有类的父亲

6.2.3、重写父类的方法

重写只能适用于实例方法,不能用于静态方法,对于静态方法,只能隐藏

大部分时候, 子类总是以父类为基础,额外增加新的成员变量和方法.

如下案例:写一个鸟类Bird.java类

public class Bird {

    public void fly(){
        System.out.println("我会飞翔,啦啦啦!!!");
    }
}

定义一个鸵鸟类Ostrich.java类继承上面的Bird类,同时重写了Bird类的fly()方法

/**
 * 定义一个鸵鸟类
 */
public class Ostrich extends Bird {
    //重写Bird类的fly()方法
    @Override
    public void fly() {
        System.out.println("我不会飞,但是我有双翅膀!!!");
    }

    public static void main(String[] args) {
        Ostrich ostrich = new Ostrich();
        ostrich.fly();
    }
}

运行结果如下:

我不会飞,但是我有双翅膀!!!

对上述代码分析:

  • 子类包含与父类同名方法的现象称为方法重写(Override). 也被称为方法覆盖.
6.2.3.1、方法的重写要遵循两同两小一大规则
  1. 两同: 方法名相同 / 形参列表相同
  2. 两小: 子类方法返回值类型应比父类方法返回值类型小或相等. / 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等.
  3. 一大: 子类方法的访问权限应比父类方法的访问权限大或相等.

知识补充:

  • 尤其需要指出, 覆盖方法和被覆盖方法要么都是类方法, 要么都是实例方法,不能一个是类方法, 一个是实例方法。
  • 如果子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写。
  • 父类引用指向子类对象时,只会调用父类的静态方法。所以,它们的行为也并不具有多态性。
  • 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可以被继承和重写,因此可以实现多态。

看如下代码就会报错:

public class Ye {

}

class Fu extends Ye{

    public static void a(){

    }
}

class Zi extends Fu{

    //这里会报错
    public void a(){

    }
}

定义测试类java是神魔意思_构造器_16

6.2.3.2、方法重写的注意点
  • 当子类覆盖了父类方法后, 子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。
  • 在子类方法中调用父类中被覆盖的方法, 则可以使用***super(被覆盖的是实例方法)*** 或者 父类类名(被覆盖的是类方法) 来作为调用者, 调用父类中被覆盖的方法.
  • 父类方法具有 private 访问权限, 则该方法对其子类是隐藏的,因此子类无法访问该方法, 也就无法重写该方法.
  • 如果子类中定义了一个与父类 private 方法具有相同的方法名 / 相同的形参列表 / 相同的返回值类型的方法, 依然不是重写,这只是在子类中重新定义了一个新的方法

请看如下代码,该代码是正确的:

public class Ye {

}

class Fu extends Ye{

    //private 访问权限, 子类不可访问该方法
    private void a(){

    }
}

class Zi extends Fu{

    //此处并不是方法重写, 所以可以增加 static 关键字
    public static void a(){

    }
}

知识扩充

  • 方法重载和方法重写在英文中分别是 overload 和 override
  • 重载主要发生在同一个类的多个同名方法之间.
  • 重写发生在子类和父类的同名方法之间.
  • 如果子类定义了一个与父类方法有相同方法名, 但参数列表不同的方法, 就会形成父类方法和子类方法的重载.

话不多说,直接代码撸起来(父类与子类方法的重载):

public class Ye {

}

class Fu extends Ye{

    public void a(int a){

    }
}

class Zi extends Fu{

    /**
     * 父类方法和子类方法发生重载
     * @param a
     * @param b
     */
    public void a(int a,int b){

    }
}
6.2.4、super关键字的学习
  • 前面也提到过,如果需要在子类方法中调用父类被覆盖的实例方法,则可以使用 super 限定来调用父类被覆盖的实例方法.
    用之前鸵鸟类的案例,为这个类添加个方法来调用父类Bird类的方法,来解析这句话。请看代码:
/**
 * 定义一个鸵鸟类
 */
public class Ostrich extends Bird {
    @Override
    public void fly() {
        System.out.println("我不会飞,但是我有双翅膀!!!");
    }

    public void method(){
        super.fly();
    }

    public static void main(String[] args) {
        Ostrich ostrich = new Ostrich();
        ostrich.method();
    }
}

结果如下:

我会飞翔,啦啦啦!!!

定义测试类java是神魔意思_静态变量_17

  • super 用于限定该对象调用它从父类继承得到的实例变量或方法,正如 this 不能出现在 static 修饰的方法中一样, super 也不能出现在 static 修饰的方法中,static 修饰的方法是属于类的,该方法的调用者可能是一个类, 而不是对象, 因而 super 限定也就失去了意义.
  • 如果在构造器中使用 super,则 super 用于限定该构造器初始化的是该对象从父类继承得到的实例变量, 而不是该类自己定义的实例变量.
  • 如果子类定义了和父类同名的实例变量,子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问到父类中被隐藏的实例变量,需要通过super 来访问父类中被隐藏的实例变量.
    如果小白还是不能够理解这几句话的,咱们可以代码撸起来,让思路更加清晰。请看如下代码:
    Father.java
public class Father {

    int a = 10;
    
}

Son.java

class Son extends Father{
     int a = 20;

     public void method(){
         System.out.println(super.a);
     }

     public static void main(String[] args) {
         Son son = new Son();
         //值为子类中的实例变量20
         System.out.println(son.a);
         //值为父类中的实例变量10
         son.method();

     }

}

解析上述代码:

Father类和Son类中都定义了名为a的实例变量,Son类中的a实例变量将会隐藏Father类中a的实例变量。用内存讲的话,实际上在创建Son类的对象时,实际上会为Son对象分配两块内存

  • 一块用于存储在 Son类中定义的 a 实例变量.
  • 一块用于存储从 Father类继承得到的 a 实例变量.
  • 如果在某个方法中访问名为 a 的成员变量, 但没有显式指定调用者, 则系统查找 a 的顺序为:
  • 查找该方法中是否有名为 a 的局部变量.
  • 查找当前类中是否包含名为 a 的成员变量.
  • 查找 a 的直接父类中是否包含名为 a 的成员变量, 依次上溯 a 的所有父类. 直到 java.lang.Object 类.
  • 如果最终不能找到名为 a 的成员变量, 则系统出现编译错误.
  • 如果被覆盖的是类变量, 在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量.
6.2.5、super调用父类构造器
  • 子类不会获得父类的构造器,但是子类构造器里可以调用父类构造器的初始化代码.
  • 在一个构造器中调用另一个重载的构造器使用 this(参数) 调用来完成.
  • 在子类构造器中调用父类构造器使用 super 调用来完成且必须在子类构造器的首行且只能调用父类构造器一次.
    话不多说,代码如下:
    Father.java
public class Father {

    public String name;
    public int age;

    public Father(String name,int age){

         = name;
        this.age = age;
    }

}

Son.java

class Son extends Father {
    public String sex;

    public Son(String name, int age, String sex) {

        //super调用父类构造器必须要在第一行
        super(name, age);
        this.sex = sex;
    }

    public static void main(String[] args) {
        Son son = new Son("张三", 23, "女");
        System.out.println( + "---->" + son.age + "---->" + son.sex);

    }

}

打印结果如下:

张三---->23---->女

  • 子类构造器执行体的第一行使用 super 显式调用父类构造器.系统将根据 super 调用里传入的实参列表调用父类对应的构造器.
  • 子类构造器执行体的第一行代码使用 this 显式调用本类中重载的构造器,系统将根据 this 调用里传入的实参列表调用本类中的另一个构造器,执行本类中另一个构造器时即会调用父类构造器.
  • 子类构造器执行体中既没有 super 调用, 也没有 this 调用, 系统将会在执行子类构造器之前, 隐式调用父类无参数的构造器.
    用代码理解,来的更快,如下所示:
class Creature {
    public Creature() {
        System.out.println("Creature无参数构造器");
    }
}

class Animal extends Creature {

    public Animal(String name) {
        System.out.println("我是Animal一个参数的构造器:--->" + name);
    }

    public Animal(String name, int age) {
        this(name);
        System.out.println("我是Animal两个参数的构造器:--->" + age);
    }
}

public class Worf extends Animal {

    public Worf(){
        super("熊猫",12);
        System.out.println("我是Worf无参数构造器");
    }

    public static void main(String[] args) {
        new Worf();
    }
}

运行结果如下:

Creature无参数构造器
我是Animal一个参数的构造器:—>熊猫
我是Animal两个参数的构造器:—>12
我是Worf无参数构造器

解析上述代码:

  • 创建任何对象总是从该类所在继承树最顶层的类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器.
  • 如果某个父类通过 this 调用了 同类中重载的构造器,就会依次执行此父类的多个构造器.
6.2.6、抽象类
6.2.6.1、抽象类和类的相同点

1、抽象类和类一样,都是可以用来继承的

2、类可以有的成分,抽象类都可以拥有【包括构造方法、static静态修饰成分等】

6.2.6.2、抽象方法

1、抽象方法没有方法体

2、抽象方法必须用abstract关键字修饰

3、有抽象方法的类必然是抽象类

4、抽象方法必须为public或者protected,缺省情况下默认为public

注意点:抽象类不一定有抽象方法

6.2.6.3、抽象类和类的不同点

1、抽象类必须用abstract关键字进行修饰,有abstract修饰的类就是抽象类!

2、抽象类可有可无抽象方法

3、抽象类虽然有构造方法但不能用来直接创建对象实例

4、抽象类不能用finalprivate修饰

5、外部抽象类不能用static修饰,但内部的抽象类可以使用static声明。

无法实例化的抽象类有什么用?

答:因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。

第五句话不理解的小伙伴,请看如下代码:

public  class AbstractDemo {

    public static void main(String[] args) {
        //向上转型
        A.B ab = new C();
        ab.method();
    }

}

/**
 * 定义一个抽象类A
 */
abstract class A{

    /**
     * 定义一个内部抽象类B
     */
    static abstract class B{

        public abstract void method();
    }

}

class C extends A.B{
    @Override
    public void method() {
        System.out.println("method方法执行了");
    }
}

解析上述代码:

当使用static声明的内部抽象类相当于一个外部抽象类,继承的时候使用外部类.内部类的形式表示类名称,这波操作属于稳中带皮。

6.2.6.4、掌握抽象类

1、抽象类就是为了继承而存在的,定义了一个抽象类,却不去继承它,创建的这个抽象类就毫无意义!

2、抽象类虽然有构造方法但不能直接被实例化,要创建对象涉及***向上转型***

3、一个普通类A继承一个抽象类B,则子类A必须实现父类B的所有抽象方法。如果子类A没有实现父类B的抽象方法,则必须将子类A也定义为为abstract类,也就是抽象类。

6.2.7、接口

接口(interface)可以说成是抽象类的一种特例,抽象类与接口这两者有太多相似的地方,又有太多不同的地方。相对的,接口更像是一种行为的抽象!

6.2.7.1、接口的特性

1、接口中的方法默认为public abstract类型

2、接口中的成员变量类型不写默认为public static final

3、接口没有构造方法

4、接口可以实现“多继承”,一个类可以实现多个接口,实现写法格式为直接用逗号隔开即可。

6.2.7.2、接口必知

1、接口中只能含有public static final变量,不写默认是public static final,用privateprotected修饰会编译失败。

2、在jdk1.7之前:接口中只能定义常量和抽象方法;在jdk1.8以后:接口中新增可以定义静态方法和默认方法,默认方法为public default其中public可以省略,这里的default却不再是修饰符;jdk1.9后:可以拥有私有的静态方法和非静态方法

6.2.7.3、抽象类和接口本质区别

1.抽象类可以有构造方法,接口中不能有构造方法。

2.抽象类中可以有任何类型成员变量,接口中只能有public static final变量

3.抽象类中可以包含非抽象的普通方法,接口中的可以有非抽象方法,比如deaflut方法和静态方法、私有静态方法、私有非静态方法

4.抽象类中的抽象方法的访问类型可以是publicprotected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。

6.抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

7.一个类可以实现多个接口,但只能继承一个抽象类。

6.3、多态
6.3.1、多态的定义:

指允许不同类的对象对同一消息做出响应,即同一消息可以根据发送的对象的不同而采用多种不同的行为。 (发送消息就是方法调用)

6.3.2、多态存在的三个必要条件

1、必须要有继承

2、必须要有重写

3、父类的引用指向子类的对象

6.3.3、实现多态的技术

动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

这句话肯定会有同学问,动态绑定是啥?感觉一脸懵逼!接下来就到我未大家解答的时候了!!!

6.3.4、动态绑定和静态绑定
  • 绑定的概念:

绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定,或者叫做前期绑定和后期绑定

  • 静态绑定(前期绑定):

针对java简单的可以理解为程序编译期的绑定,这里特别说明一点,java当中的方法只有finalstaticprivate修饰和构造方法是静态绑定,因为被这些修饰的方法是不能够被重写的,所以在编译时就可以确定他们的值。

  • 动态绑定(后期绑定):

在运行时根据具体对象的类型进行绑定。

实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。

  • 动态绑定的过程:

1、虚拟机提取对象的实际类型的方法表;

2、虚拟机搜索方法签名;

3、调用方法。

  • 总结:
    前面已经说了对于java当中的方法而言,除了final,static,private和构造方法是前期绑定外,其他的方法全部为动态绑定,而动态绑定的典型发生在父类和子类的转换声明之下:比如:
    Parent p = new Children();
    关于多态中方法的调用写个案例:如下所示
    Father.java
public class Father {
    public void method(){
        System.out.println("父类方法,对象类型:" + this.getClass());
    }
}

Son.java

public class Son extends Father{

    public static void main(String[] args) {
        //向上转型
        Father father = new Son();
        father.method();
    }
}

结果如下:

父类方法,对象类型:包名.Son

解析:声明的是父类的引用,但是执行的过程中调用的是子类的对象,程序首先寻找子类对象的method方法,但是没有找到,于是向上转型去父类寻找

Father.java

public class Father {
    public void method(){
        System.out.println("父类方法,对象类型:" + this.getClass());
    }
}

Son.java

public class Son extends Father{

    @Override
    public void method(){
        System.out.println("子类方法,对象类型:" + this.getClass());
    }


    public static void main(String[] args) {
        //向上转型
        Father father = new Son();
        father.method();
    }
}

结果如下:

子类方法,对象类型:包名.Son

解析:由于子类重写了父类的method方法,根据上面的理论知道会去调用子类的method方法去执行,因为子类对象有method方法而没有向上转型去寻找。

关于多态中成员变量的调用写个案例:如下所示

前面的理论当中已经提到了java的绑定规则,由此可知,在处理java类中的成员变量时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性还是父类的属性,代码如下:

Father.java

public class Father {

    protected String name = "父亲的属性";

    public void method(){
        System.out.println("父类方法,对象类型:" + this.getClass());
    }
}

Son.java

public class Son extends Father{

    protected String name = "儿子的属性";

    @Override
    public void method(){
        System.out.println("子类方法,对象类型:" + this.getClass());
    }


    public static void main(String[] args) {
        //向上转型
        Father father = new Son();
        System.out.println("调用的成员为:" + father.name);
    }
}

结果如下:

调用的成员为:父亲的属性

解析:这个结果表明,子类的对象调用到的是父类的成员变量。所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法。

现在试图调用子类的成员变量name,该怎么做?最简单的办法是**将该成员变量封装成方法getter形式。** 代码如下:

Father.java

public class Father {

    protected String name = "父亲的属性";

    public String getName() {
        return name;
    }

    public void method(){
        System.out.println("父类方法,对象类型:" + this.getClass());
    }
}

Son.java

public class Son extends Father{

    protected String name = "儿子的属性";

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void method(){
        System.out.println("子类方法,对象类型:" + this.getClass());
    }


    public static void main(String[] args) {
        //向上转型
        Father father = new Son();
        System.out.println("调用的成员为:" + father.getName());
    }
}

结果如下:

调用的成员为:儿子的属性

  • 向上转型(自动转换)

上面已经描述,这里就不再赘述

  • 向下转型(强制转换)
  1. 使用向下转型的情形:子类继承父类,子类拥有父类没有的方法。例如:Animal是父类,Dog是子类,eat()方法是Dog子类特有的方法

错误演示:

Animal a = new Dog();

a.eat();

正确演示:

//第一种方法(其实和下面是一样的,没区别,只是简写而已):

Dog dog = (Dog)a;

dog.eat();

//第二种方法(将父类的引用a强制转换成Dog类型):

((Dog)a).eat();

Animal.java类

public class Animal {

    public void run(){
        System.out.println("所有动物都能跑。。。。。。");
    }
}

Dog.java类

public class Dog extends Animal{
    @Override
    public void run() {
        System.out.println("狗子在跑......");
    }

    public void eat(){
        System.out.println("我吃了一波狗粮......");
    }
}

测试类:

public class Main {
    public static void main(String[] args) {
        //第一种格式
        Dog dog = (Dog) new Animal();
        dog.eat();

        //第二中格式;
        Animal animal = new Dog();
        ((Dog) animal).eat();
    }
}

如果,此文章对你有所帮助,请帮忙点个赞呗! 谢谢!!!

定义测试类java是神魔意思_构造器_18