JavaSE(8)-细节狂魔:21k字长篇深入理解类和对象

目录

前言

1. 面向对象的初步认知

1.1 什么是面向对象

1.2 面向对象与面向过程

2. 类定义和使用

2.1 简单认识类

2.2 类的定义格式

3. 类的实例化

3.1 什么是实例化

3.2 类和对象的说明

3.3 总结

4. this引用

4.1 为什么要有this引用

4.2 什么是this引用

4.3 this引用的特性  

5. 对象的构造及初始化

5.1 如何初始化对象

5.2 构造方法

5.3 默认初始化

5.4 就地初始化

6. 封装

6.1 封装的概念

6.2 访问限定符

6.3 封装扩展-包

7. static成员

7.1 引子

7.2 static修饰成员变量

7.3 static修饰成员方法

7.4 static扩展

7.5 static成员变量初始化

8. 代码块

8.1 代码块概念以及分类

8.2 普通代码块

8.3 构造代码块

8.4 静态代码块

9. 内部类

9.1 内部类的概念

9.2 内部类的分类

9.3 成员内部类

9.4 局部内部类

9.5 匿名内部类


前言

⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

这章我们就要进入类和对象的学习了,这和前面的C语言就已经开始分道扬镳了,需要我们认真理解每个细节。 笔者刚开始学习类和对象的时候人也是懵的,但只要耐心啃几遍就基本能理解了。如果本文对你有帮助,请点赞收藏评论关注四连,这对我帮助真的很大。本人只是初学者水平有限,如果有误,欢迎大佬在评论区指正。

本文敲了两天,干货满满,开始食用吧。

⭐⭐⭐⭐⭐⭐⭐ 透过无所有看见所有,于无所希望中得救⭐⭐⭐⭐⭐⭐⭐⭐⭐

1. 面向对象的初步认知

1.1 什么是面向对象

Java是一门纯面向对象的语言(Object Oriented Program,简称OOP),在面向对象的世界里,一切皆为对象。面向对象是解决问题的一种思想,主要依靠对象之间的交互完成一件事情。

1.2 面向对象与面向过程

1.2.1 面向过程

传统的方式:注重的是洗衣服的过程,少了一个环节可能都不行。
而且不同衣服洗的方式,时间长度,拧干方式都不同,处理起来就比较麻烦。如果将来要洗鞋子,那就是另一种方式。 按照该种方式来写代码,将来扩展或者维护起来会比较麻烦

1.2.2 面向对象

以面向对象方式来进行处理,就不需要关注洗衣服的过程。
具体洗衣机是怎么来洗衣服,如何来甩干的,用户不用去关心,只需要将衣服放进洗衣机,倒入洗衣粉,启动开关即可,通过对象之间的交互来完成的。
面向过程就是所有的过程都要一步一步的自己去实现,
面向对象需要去 找对象、创建对象、使用对象,不注重过程是如何实现的。
注意:面向过程和面向对象并不是一门语言,而是解决问题的方法,没有那个好坏之分,都有其专门的应用场景。

1.2.3 对象从何而来

我们如果要创建一个对象,首先就需要有一个类。
我们认为的对象可以看成是实体,而类则可以看作是一个模板,

有了类这个模板以后,就可以创建对象了。

2. 类定义和使用

2.1 简单认识类

类是用来对一个实体(对象)来进行描述的
主要描述该实体 ( 对象 ) 具有哪些属性 ( 外观尺寸等 ) ,哪些功能 ( 用来干啥) ,描述完成后计算机就可以识别了。

举个例子,建一个房子,房子的图纸就是一个类,而造出来的房子就是一个对象。也就是说,通过类,我们才能把类描述的抽象的概念实例化成对象。

2.2 类的定义格式

在java中定义类时需要用到class关键字。
具体语法如下:

class为定义类的关键字,ClassName为类的名字,需要以大驼峰形式命名,{}中为类的主体。

类中包含的内容称为类的成员。属性主要是用来描述类的,称之为类的成员属性或者类的成员变量。方法主要说明类具有哪些功能,称为类的成员方法。

代码示例

class PetDog {
   // 狗的属性
    public String name;//名字
    public String color;//颜色
   // 狗的行为
    public void barks() {
        System.out.println(name + ": 旺旺旺~~~");
    }
    public void wag() {
        System.out.println(name + ": 摇尾巴~~~");
    }
}

注意事项

  • 类名注意采用大驼峰定义
  • 成员前写法统一为public,后面介绍修饰限定符的时候你就懂了。
  • 此处写的方法没有带 static 关键字,后面会详细解释
  • 建议一个java文件当中只定义一个类
  • 一个java文件可以定义多个类,但是被public修饰的只能有一个!
  • main方法所在的类一般要使用public修饰
  • public修饰的类必须要和文件名相同!
  • 不要轻易去修改public修饰的类的名称,否则可能导致异常。如果要修改,通过开发工具修改。

3. 类的实例化

3.1 什么是实例化

定义了一个类,就相当于在计算机中定义了一种新的类型
与int,double类似,只不过int和double是java语言自带的内置类型,而类是用户自定义了一个新的类型,比如上述的PetDog就是类(一种新定义的类型)。有了这些自定义的类型之后,就可以使用这些类来定义实例(或者称为对象)。
用类类型创建对象的过程,称为类的实例化。
在java中采用new关键字,配合类名来实例化对象。

示例代码(通过PetDog实例化两个对象):

public class Main{
    public static void main(String[] args) {
        PetDog dog1 = new PetDog(); //通过new实例化对象
//调用方法的时候才会用小括号(),其实这个也是调用方法——>构造方法
//至于什么是构造方法,以及构造方法的相关知识点,会在下面介绍
        dog1.name = "熊大";
        dog1.color = "棕红";
        dog1.barks();
        dog1.wag();

        PetDog dog2 = new PetDog();
        dog2.name = "二狗";
        dog2.color = "黑黄";
        dog2.barks();
        dog2.wag();
    }
}
//输出结果: 
//熊大: 旺旺旺~~~ 
//熊大: 摇尾巴~~~ 
//二狗: 旺旺旺~~~ 
//二狗: 摇尾巴~~~

注意事项

  • new 关键字用于创建一个对象的实例,
  • 使用 . 来访问对象中的属性和方法
  • 同一个类可以创建多个实例
  • 如果成员变量没有赋予初值,那么就会默认初始为对应的0值,引用类型是null,boolean类型是false,char类型是\u0000,整形对应0,浮点型对应0.0。如果是局部变量的话,没有初始化必然会报错

3.2 类和对象的说明

  • 类可以理解为模型,用来对一个实体进行描述,限定了类有哪些成员。
  • 类是一种自定义的类型,可以用来定义变量,但是在java中用类定义出来的变量我们称为对象。
  • 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,用于存储类成员变量,在堆区开辟内存。
  • 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,只有实例化出的对象才能实际存储数据,占用物理空

3.3 总结

3.3.1 如何定义一个类

class Student{
    //字段,属性,成员变量(定义在类的内部,方法的外部的)。
    String name;
    int age;
    //成员方法
    public void eat(){
        System.out.println(name+"正在吃饭");
    }
}

3.3.2 如何通过这个类来实例化对象

 Student student1 = new Student();
 Student student2 = new Student();

通过new关键字来实例化对象,只要new一下就会给这个对象分配内存:

 3.3.3 怎么去访问实例化出来的 对象的属性和方法

        student1.属性;
        student1.方法;

4. this引用

4.1 为什么要有this引用

先看一个日期类的例子:

public class Date {
    public int year;
    public int month;
    public int day;
//用于设置日期的方法
    public void setDay(int y, int m, int d){
        year = y;
        month = m;
        day = d;
    }
//用于打印日期的方法
    public void printDate(){
        System.out.println(year + "/" + month + "/" + day);
    }
    public static void main(String[] args) {
// 构造三个日期类型的对象 d1 d2 d3
        Date d1 = new Date();
        Date d2 = new Date();
        Date d3 = new Date();
// 对d1,d2,d3的日期设置
        d1.setDay(2020,9,15);
        d2.setDay(2020,9,16);
        d3.setDay(2020,9,17);
// 打印日期中的内容
        d1.printDate();
        d2.printDate();
        d3.printDate();
    }
}

运行结果如下图所示:

以上代码定义了一个日期类,然后在main方法中创建了三个对象,并通过Date类中的成员方法对对象进行设置和打印,代码整体逻辑非常简单,没有任何问题。但细思极恐:


1. 形参名若不小心与成员变量名相同
   public void setDay(int year, int month, int day){
        year = year;
        month = month;
        day = day;
    }
那函数体中到底是谁给谁赋值?成员变量给成员变量?参数给参数?参数给成员变量?成员变量参数?我们运行程序,却发现打印的是: 
大无语事件发生了!为什么同名就不行事了呢?其实仔细想想,也很好理解的。
代码中的 year = year;month = month;day = day;

传的参数year、month、day它们是局部变量,其作用域在setDate方法内部,我们知道就近原则,所以局部变量优先全局变量使用,这就相当于是自己给自己赋值了,但是没有给处于setDate方法之外的成员变量year、month、day赋值。所以,存储在堆上面的对象的year、month、day并没有被赋值,默认赋值为0。所以才会导致这个情况。


2.三个对象都在调用setDate和printDate函数,但是这两个函数中没有任何有关对象的说明,setDate和printDate函数如何知道打印的是那个对象的数据呢?

4.2 什么是this引用

java编译器给每个“成员方法”增加了一个隐藏的引用类型参数,该引用参数指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。 这个引用就是this引用,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成, 用户在实现代码时一般不需要显式给出。通过this引用,我们就可以规避上面的两个问题了。

代码示例
public class Date {
    public int year;
    public int month;
    public int day;
    public void setDay(int year, int month, int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public void printDate(){
        System.out.println(this.year + "/" + this.month + "/" + this.day);
    }
}
    public static void main(String[] args) {
        Date d = new Date();
        d.setDay(2020,9,15);
        d.printDate();
    }

我们来验证一下,this所引用的就是当前调用成员方法的对象

通过打断点调试,我们发现引用变量d指向的空间 和this引用指向的空间是同一块内存,所以这也说明了,this所引用的就是当前调用成员方法的对象

4.3 this引用的特性  

  • this的类型:this代表当前对象的引用,对应类类型引用,即哪个对象调用就是哪个对象的引用类型
  • this只能在"成员方法"中使用!
  • 在"成员方法"中,this只能引用当前对象,不能再引用其他对象,具有final属性
  • 当参数和成员的名字冲突的时候,可以用以区别(如果名字不冲突的时候,也可以用this),至于到底以后的程序是否要加上this,建议是:在当前类当中去访问自己的成员变量和属性的时候,可以去把this加上(不是硬性要求,但最好这样,能规避未知的风险)
  • this是“成员方法”的参数列表中的第一个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器会负责将 调用成员方法对象的引用 传递给该成员方法,this负责来接收。

在代码层面来简单演示--->注意:下图右侧中的Date类也是可以通过编译的!

从字节码层面来简单证明

 此处给大家介绍了this的作用以及特性,还有一些其他特性后序展开。


this可以为空吗

正常的this永远不能是null

要调用类实例的方法,实例必须存在。该实例隐式的由this引用作为参数传递给方法。如果thisnull,那么就没有实例来调用方法。


this的三个用途

this.data   使用数据

this.func   使用方法

this()         调用构造方法

那么,什么是构造方法呢?接下来我们就要来学习构造方法了。

5. 对象的构造及初始化

5.1 如何初始化对象

通过前面知识点的学习知道,在 Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。

 要让上述代码通过编译,非常简单,只需在正式使用a之前,给a设置一个初始值即可。如果想要初始化对象,那么可以用点操作符,去一个一个初始化,也可以调用所创建的方法去初始化

通过上述例子发现两个问题:

1. 每次对象创建好后调用SetDate方法设置具体日期,比较麻烦,那对象该如何初始化?

2. 局部变量必须要初始化才能使用,为什么字段声明之后没有给值依然可以使用?
记住这两个问题,接下来我们将慢慢揭开他们神秘的面纱

5.2 构造方法

5.2.1 概念
  • 构造方法(也称为构造器)是一个特殊的成员方法。
  • 语法:修饰限定符 类名 (参数列表){具体实现}
示例代码

5.2.2 构造方法的特性

1.名字必须与当前类名相同一旦与类名不同了,就会被识别为普通方法,然后报错:方法声明无效;需要返回类型


2.构造方法没有返回值类型,设置为void也不行,一旦设置了返回值类型,就会被识别为普通的方法,只能通过.操作符来调用了。


3.一般情况下使用public修饰限定符来修饰,特殊场景下会被private修饰(后序讲单例模式时会遇到)


4.在创建对象时,构造方法由编译器自动调用,并且在整个对象的生命周期内只会在创建对象的时候调用一次!


5.构造方法的作用是给对象中的成员进行初始化,并不负责给对象开辟空间。


6.构造方法可以重载(用户根据自己的需求提供不同参数的构造方法),根据传入参数的一一对应来自动调用对应的构造方法。我们知道重载的条件是方法名相同,参数列表不同,与返回值无关,而构造方法是没有任何返回值的,所以能构成重载。

public class Date {
    public int year;
    public int month;
    public int day;
    // 无参构造方法
    public  Date(){
        this.year = 1900;
        this.month = 1;
        this.day = 1;
    }
    //带有三个参数的构造方法
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public void printDate(){
        System.out.println(year + "-" + month + "-" + day);
    }
    public static void main(String[] args) {
        Date d = new Date();
        Date d2=new Date(2022,3,20);
        d.printDate();
        d2.printDate();
    }
}
上述两个构造方法:名字相同,参数列表不同,因此构成了方法重载

7.如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的。

默认生成的构造方法形如:

public Date(){

}
一旦用户定义了任何一个构造方法,编译器则不再自动生成,证明过程如下:
public class Date {
    public int year;
    public int month;
    public int day;
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public void printDate(){
        System.out.println(year + "-" + month + "-" + day);
    }
    public static void main(String[] args) {
        Date d = new Date();
        d.printDate();
    }
}
/*
Error:(26, 18) java: 无法将类 extend01.Date中的构造器 Date应用到给定类型;
需要: int,int,int
找到: 没有参数
原因: 实际参数列表和形式参数列表长度不同
*/
如果编译器会生成,则生成的构造方法一定是无参的
则此代码创建的对象是可以通过编译的
但实际情况是:编译器报错。也就说明了没有默认生成无参的构造方法了。

8. 构造方法中,可以通过this调用其他构造方法来简化代码
public class Date {
    public int year;
    public int month;
    public int day;

    // 无参构造方法
// 此处可以在无参构造方法中通过this调用带有三个参数的构造方法
//但是this(1900,1,1);必须是构造方法中第一条语句
    public Date(){
//System.out.println(year); 注释取消掉,编译会失败,this调用构造方法,必须放在第一行
        this(1900, 1, 1);
    }

    // 带有三个参数的构造方法
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
} 
注意:
  • this(...)必须是构造方法中第一条语句,不放在第一条就会报错
  • 不能形成环

5.2.3 小结

5.3 默认初始化

学习了构造方法后,我们已经能够完美解决上文第一个问题了,在上文中提出的第二个问题:为什么局部变量在使用时必须要初始化,而成员变量可以不用呢?

public class Date {
    public int year;
    public int month;
    public int day;
    public Date(int year, int month, int day) {
// 成员变量在定义时,并没有给初始值, 为什么就可以使用呢?
        System.out.println(this.year);
        System.out.println(this.month);
        System.out.println(this.day);
    }
    public static void main(String[] args) {
// 此处a没有初始化,编译时报错:
// Error:(24, 28) java: 可能尚未初始化变量a
// int a;
// System.out.println(a);

        Date d = new Date(2021,6,9);
    }
}

要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情:

在程序层面这只是简单的一条语句,在JVM层面却需要做好多事情,下面简单介绍下:

  • 检测对象对应的类是否加载了,如果没有加载则加载
  • 为对象分配内存空间
  • 处理并发安全问题 (比如:多个线程同时申请对象,JVM要保证给对象分配的空间不冲突)
  • 初始化所分配的空间(即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值)

  • 设置对象头信息(关于对象内存模型后面会介绍)
  • 调用构造方法<>,给对象中各个成员赋值

5.4 就地初始化

概念:在声明成员变量时,就直接给出了初始值。

注意:代码编译完成后,编译器会将所有给成员初始化的这些语句添加到各个构造函数中。

总结:用就地初始化还是默认初始化还是构造初始化,根据业务需求而定。

6. 封装

6.1 封装的概念

面向对象程序有三大特性:封装、继承、多态

而在类和对象阶段,主要研究的就是封装特性。何为封装呢?简单来说就是套壳屏蔽细节

例如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、键盘,显示器,USB 插孔等,让用户来和计算机进行交互,完成日常事务。但实际上:电脑真正工作的却是CPU 、显卡、内存等一些内部的硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的, CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。
因此计算机厂商在出厂时,在外部套上壳 子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互

6.2 访问限定符

6.2.1 基本概念

Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认知,而访问权限用来控制方法或者字段能否直接在类外使用。

Java中提供了四种访问限定符:

  • public:在哪里都能访问,不能轻易改名字
  • default:什么限定符都不加时的默认权限,对于同一个包可以随意使用,对于其他包来说就不能访问了
  • private:只能在当前类使用,其他地方无法访问
  • protected:主要是用在继承中,继承部分详细介绍,这里留个伏笔
  • 访问权限除了可以限定类中成员的可见性,也可以控制类本身的可见性,外部类只能用public修饰或者默认修饰。

6.2.2 Get And Setter

有的私有数据我们不想直接给别人展示出来,就可以设置为private类型,那我们该如何使用private变量呢?

Idea给我们提供了非常便捷的自动生成代码的功能,通过get and setter 可以便利的为访问私有变量提供公开的方法。

 

 通过Getter and Setter生成的代码如下:


6.2.2 自动生成构造方法

感受到Idea的强大了吗?当然Generate还有其他的功能,比如生成构造方法。也就是右击代码,然后点击Constructor,选择我们需要的变量,就会自动为这些变量生成构造方法了。


6.2.3 自动生成重写的toString

你是否有想过怎么直接打印对象的内容呢?我们如果想要打印stu1对象,用sout就是:

System.out.println((stu1));

毫无疑问,由于stu1是一个局部变量,也是一个引用变量,里面存储的是地址。

为什么打印的是Student@一堆乱七八糟的东西呢?这个时候我们就要跳到源码去研究研究了。

按住ctrl,然后把鼠标放到printlen方法上面,我们就能查看源码

println的源码如下: 

看到这个,是不是晕乎乎的?这是什么鬼嘛,没关系,笔者第一次看也是懵的,但代码都是人写的,多看几遍总能理解的。

我们发现传入的是一个Object的x,然后是将x经过了某种转换存入了s,然后就是打印s,换行。那么问题的关键就在于这个转换是什么了。我们继续按住ctrl,然后鼠标放到valueOf上面,跳到了如下源码:

这啥啊,那就分析呗,传入的参数是obj,如果obj为空引用,就输出null,否则就调用obj.toSting()。那么就接着来吧,按住ctrl,鼠标放到toSting上面

这就容易理解了吧,这其实返回的就是字符串类型的类名@哈希值

我们只需要重写toString方法,就可以打印出想要的目标内容了。

通过Generate可以快速重写toString

 默认重写的toString方法如下所示 

代码运行结果如下

由于返回值是字符串类型,所以里面填啥都可以。如下所示:

 关于重写我们在多态章节将会详细介绍。

6.3 封装扩展-包

6.3.1 包的概念

在面向对象体系中,提出了软件包的概念,即:为了更好的管理类,我们把多个类收集在一起成为一组,称为软件包软件包有点类似于目录。

比如:为了更好的管理电脑中的"学习资料",可以对科目进行分类,也可以对某个科目的文件夹下的资料进行更详细的分类。

在Java中也引入了包,包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式。
比如:一个包中的类不想被另一个包中的类使用,可以使用包访问权限default(默认权限啥也不加)修饰。
包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在不同的包中即可。

6.3.2 导入包中的类

Java 中提供了很多现成的类供我们使用。例如Date类:可以使用 java.util.Date 导入 java.util 这个包中的 Date 类。

public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

但是这种写法比较麻烦一些,可以使用 import语句导入包。

import java.util.Date;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

如果需要使用 java.util 中的其他的类,可以使用 import java.util.*,这也就是能够用到java.util包底下的全部的类了,但不是全部都加载进去,全部加载显然浪费了内存,java的机制是用到啥就加载啥。

import java.util.*;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

一键导入爽是爽,但是我们建议显式的指定要导入的类名。否则还是容易出现冲突的情况

在这种情况下就需要指定完整的类名了

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        System.out.println(date.getTime());
    }
}
我们还可以使用import static导入包中静态的方法和字段。 但不常用,这里只稍微提及一下
import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
// 静态导入的方式写起来更方便一些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
    }
}
import 和 C++ 的 #include 差别很大,C++ 必须 #include 来引入其他文件内容,但是 Java 不需要。import 只是为了写代码的时候更方便。import 更类似于 C++ 的 namespace 和 using。
6.3.3 自定义包
基本规则
  • 在文件的最上方加上一个 package 语句指定源代码在哪个包中。
  • 包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(例如 com.baidu.www)
  • 包名要和代码路径相匹配。例如创建 com.bit.demo1 的包,那么会存在一个对应的路径 com/bit/demo1 来存储代码。
  • 如果一个类没有 package 语句,则该类被放到一个默认包中。
操作步骤
1. 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包

2. 在弹出的对话框中输入包名,回车

3. 在包中创建类,右键包名 -> 新建 -> 类,然后输入类名即可

4. 此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了

5.对于初次使用IDEA的人,可能在IDEA中的包是这样显示的:

只需要三个步骤就能像上面一样显示了


6.3.4 包的访问权限控制举例
我们在 包Demo1 底下创建了两个java文件,分别是Test和Test2。然后在Test中定义了Student类。

我们在Test2中创建一个对象test2,对数据进行访问 ,发现只有private修饰的age不能访问,包访问权限和公开权限都能正常访问。

在不同的包中创建对象会怎么样呢?

我们在另一个包Demo2中的java文件Test3中,尝试着创建一个Student类型的对象,发现连创建都创建不了了,直接无法识别了: 

这是因为我们没有给Student类加任何访问修饰限定符,这个类默认就是包访问权限,只能在同一个包里面访问,所以也就无法识别了。而对于外部类,只能是public或者包访问权限,我们给Student的访问修饰限定符改为public,public修饰的类的类名必须和文件名一样,所以我们还需要用前面所说的命名方法把Test重命名为Student,这样再去访问,就能正常识别了:

我们知道,name是public修饰的,所以在哪里都能用,而sex啥也没加,是包访问权限,所以只能在Demo1底下使用,age是private的,只能在Student类里面使用。

相信到这里,你已经对这三种访问修饰限定符了如指掌了。


6.3.5 常见的包
  1. java.lang    系统常用基础类(StringObject),此包从JDK1.1后自动导入。
  2. java.lang.reflflect   java 反射编程包。
  3. java.net   进行网络编程开发包。
  4. java.sql   进行数据库开发的支持包。
  5. java.util   是java提供的工具程序包。(集合类等) 非常重要
  6. java.io   I/O编程开发包。

7. static成员

7.1 引子

我们描述如果要一个学生, 每个学生对象都有自己特有的名字、性别,年龄,学分等成 员信息,这些信息就是对不同学生来进行描述的,代码示例如下:
public class Student{
        public String name;
        public String gender;
        public int age;
        public double score;

//用于初始化的构造方法
     public Student(String name, String gender, int age, double score) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.score = score;
    }

    public static void main(String[] args) {
        Student s1 = new Student("Li Lei", "男",  18, 3.8);
        Student s2 = new Student("Han MeiMei", "女", 19, 4.0);
        Student s3 = new Student("Jim", "男", 18, 2.6);
    }
}

我们用Student类实例化三个对象s1s2s3:

public class Student{
    public static void main(String[] args) {
        Student s1 = new Student("Li leilei", "男", 18, 3.8);
        Student s2 = new Student("Han MeiMei", "女", 19, 4.0);
        Student s3 = new Student("Jim", "男", 18, 2.6);
    }
}

如果这三个同学是同一个班的,能否给类中再加一个成员变量,只通过这一个成员变量来保存这三个同学的班级呢?我们试试:
生成了三份classes,所以肯定是不行的。
我们在Student类中定义的成员变量,在每个对象中都会包含一份(称之为实例变量)
也就是说,如果在Student里面再加个表示班级的成员变量,就会创建三份实例变量。因为需要使用这些信息来描述具体的学生。
而现在要表示学生的班级,班级的属性并不需要每个学生对象中都存储一份,而是需要让所
有的学生来共享。这个时候就要引入static了。
在Java中,被static修饰的成员,称为静态成员或者类成员,其不属于某个具体的对象,是所有对象所共享的

7.2 static修饰成员变量

static修饰的成员变量,称为静态成员变量。

静态成员变量最大的特性:不属于某个具体的对象,是所有对象所共享的

【静态成员变量特性】
  • 静态成员不属于某个具体的对象,是类的属性,是所有对象共享的,不存储在某个对象的空间中
  • 既可以通过对象访问,也可以通过类名访问,但一般更推荐使用类名访问(使用对象访问合法但不合理)。
  • 类变量存储在方法区当中,而不是存储在堆中。
  • 生命周期伴随类的一生(即:随类的加载而创建,随类的卸载而销毁)
【示例代码】
public class Student {
    public String name;
    public String gender;
    public int age;
    public double score;
    public static String classes = "2111班";
    public Student(String name, String gender, int age, double score) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.score = score;
    }
    public static void main(String[] args) {
// 静态成员变量可以直接通过类名访问
        System.out.println(Student.classes);
        Student s1 = new Student("Li Lei", "男", 18, 3.8);
        Student s2 = new Student("Han MeiMei", "女", 19, 4.0);
        Student s3 = new Student("Jim", "男", 18, 2.6);
// 也可以通过对象访问:但是classRoom是三个对象共享的
        System.out.println(s1.classes);
        System.out.println(s2.classes);
        System.out.println(s3.classes);
    }
}

通过Debug调试,在监视窗口中可以看到,静态成员变量并没有存储到某个具体的对象中。

而是存储到了方法区:

7.3 static修饰成员方法

一般类中的数据成员都设置为 private ,而成员方法设置为 public ,那设置成private之后, Student 类中 classes 属性如何在类外访问呢?
Student类的代码
public class Student {
    private String name;
    private String gender;
    private int age;
    private double score;
    private static String classes = "2111班";
}

在同一个包中新定义一个TestStudent文件

public class TestStudent {
    public static void main(String[] args) {
        System.out.println(Student.classes);
    }
}

运行,然后发现,好家伙,报错了

那被private限定符修饰的static属性应该如何访问呢?

在Java中,被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的。静态成员一般是通过静态方法来访问的,当然也可以用普通的方法访问。

使用静态方法来访问静态变量:


使用普通方法来访问静态变量(必须依赖于对象):


静态方法特性
  • 静态方法不属于某个具体的对象,是一种类方法
  • 可以通过对象调用,也可以通过类名.静态方法名(参数)方式调用,更推荐使用后者
  • 静态方法没有隐藏的this引用参数,没有依赖于任何对象。因此不能在静态方法中访问任何非静态成员变量。

即使显式的在static方法中的参数列表中用this接受,也无济于事,因为java本身就不允许this在静态方法中使用:

  • 在静态方法中不能调用任何非静态方法,因为非静态方法是依赖于对象的,对象通过this参数进行指代,而在静态方法中是无法传递this引用的。但普通方法却可以使用静态方法以及静态的成员变量。
  • 静态方法无法重写,不能用来实现多态!(暂时不用管,后续多态章节会进行详细讲解)。

一个有意思的问题:main方法可以不加static吗?

对于JVM来说,可以实现加static,当然也可以实现不加static。

但Java官方是老大,指定你必须是加static,你在IDEA里面不加static直接找不到运行入口了。

7.4 static扩展

​​​​​​​class Student{
    public String name;
    public String gender;
    public int age;
    public double score;
    public static String classes;
 
    public static void func(){
        System.out.println("静态的成员方法被调用了");
    }
}
 
public class Test {
    public static void main(String[] args) {
        Student student1 = null;
        student1.classes = "2111班";
        System.out.println(student1.classes);
    }
}

我们第一眼看过去,student1里面存了个null,那使用了空指针来访问,不就会发生空指针异常么???然而我们运行,程序却没有报错,为什么呢?其实很好理解,student1不指向任何对象,而我们的classes是类变量,是依赖于类的,而不是依赖于对象的。那么即使student不指向任何的对象,它也是Student类的,classes只依赖于类,所以能够正常使用。

在实际应用中,我们也要规避写出这样奇怪的代码,直接用类名去访问就可以了。


静态的不依赖于对象,非静态的依赖于对象。

所以我们在main方法里面,要用非静态的方法,就必须new一个对象。

7.5 static成员变量初始化

7.5.1 能否用构造方法初始化

静态成员变量一般不会放在构造方法中来初始化,构造方法中初始化的是与对象相关的实例属性。
当然你非要这样也没错,但还是那句话,合法但不合理,就像下面这样,你非要用的话,看着不会很别扭吗???

 

所以咱还是尽量少写这种奇葩代码吧,进公司容易挨骂。那么如何初始化静态成员变量呢?


静态成员变量的初始化分为两种:就地初始化和静态代码块初始化。
1. 就地初始化
就地初始化指的是:在定义时直接给定初始值
    private static String classes="2111班";

2. 静态代码块初始化

这时候你就疑惑了,那啥是代码块呢?咱接着往下看吧

8. 代码块

8.1 代码块概念以及分类

8.1.1 概念

使用 {} 框住的一段代码称为代码块

8.1.2 分类

根据代码块定义的位置以及关键字,又可分为以下四种:
  • 普通/本地代码块
  • 构造代码块
  • 静态代码块
  • 同步代码块(后续多线程部分再谈,暂时用不上)

我们描述一个代码块的时候,最好把他的类型说出来,避免误会。

8.2 普通代码块

普通代码块:定义在方法中的代码块,又可称为本地代码块。

这种用法较少见,稍微了解一下即可。

 8.3 构造代码块

构造代码块:定义在类中的代码块(不加修饰符)。构造代码块也叫:实例代码块。构造代码块一般用于初始化实例成员变量。


【代码示例】
创建一个Student类

在另一个类里面用Student类实例化一个对象

运行结果如下

于是得出结论

实例代码块优先于构造方法执行,因为编译完成后,编译器会将实例代码块中的代码拷贝到每个构造方法第一条语句前。

从字节码的角度来理解

8.4 静态代码块

使用static定义的代码块称为静态代码块。一般用于初始化静态成员变量。


【示例代码】

public class Student{
    private String name;
    private String gender;
    private int age;
    private double score;
    private static String classRoom;
    //实例代码块
    {
        this.name = "张三";
        this.age = 18;
        this.gender = "男";
        System.out.println("实例代码块被调用了");
    }
    // 静态代码块
    static {
        classRoom = "2111班";
        System.out.println("静态代码块被调用了");
    }
    public Student(){
        System.out.println("构造方法被调用了");
    }
    public static void main(String[] args) {
        Student s1 = new Student();
        Student s2 = new Student();
    }
}

运行结果如下

我们可以发现,静态代码块只调用了一次,并且优先实例代码块和构造方法执行。


【注意事项】
  • 静态代码块不管生成多少个对象,其只会执行一次
  • 静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的

Java代码在经过编译器编译之后,如果要运行必须先要经过类加载子系统加载到JVM中才能运行。过程如下所示,我们初步了解即可。

在链接阶段第二步准备中会给静态成员变量开辟空间,并设置为默认值,在初始化阶段,会执行静态代码块中的代码。(关于类加载过程之后会在JVM中会详细讲解)

  • 如果一个类中有多个静态代码块,那么执行的顺序和定义的顺序有关。定义在后面的静态代码块 会把定义在前面的给覆盖掉(只是赋值覆盖,像打印之类的不会覆盖)
  • 实例代码块只有在创建对象时才会执行,而静态代码块是只要加载类就会执行。

9. 内部类

9.1 内部类的概念

在 Java 中,可以将一个类定义在另一个类或者一个方法的内部,
前者称为内部类,后者称为外部类。内部类也是封装的一种体现。
public class OutClass {
    class InnerClass{
    }
}
// OutClass是外部类
// InnerClass是内部类
【注意事项】
  • 定义在class 类名{}花括号外部的,即使是在一个文件里,都不能称为内部类
public class A{ 
}

class B{ 
}// A 和 B是两个独立的类,彼此之前没有关系
  • 内部类和外部类共用同一个java源文件,但是经过编译之后,内部类会形成单独的字节码文件

9.2 内部类的分类

我们先来看下,内部类都可以在类的那些位置进行定义
public class OutClass {
    // 成员位置定义:未被static修饰 ---> 实例内部类
    public class InnerClass1{
    }
    // 成员位置定义:被static修饰 ---> 静态内部类
    static class InnerClass2{
    }
    public void method(){
// 方法中也可以定义内部类 ---> 局部内部类:几乎不用
        class InnerClass5{
        }
    }
}

注意:内部类在日常开发中使用并不是很多,日常开始中使用最多的是匿名内部类,在数据结构部分会大量使用内部类。

以后如果谈起内部类的时候,需要带上前缀,因为其性质都不尽相同

9.3 成员内部类

9.3.1 概念

在外部类中,内部类定义位置 与 外部类成员 所处的位置相同,因此称为成员内部类


9.3.2 实例内部类

即未被static修饰的成员内部类。

创建一个实例内部类:

public class OutClass {
//普通成员变量(实例成员变量)
    private int a;
    static int b;
    int c;
    class InnerClass{
//class InterClass 这个类就叫做 实例内部类
        }
    }

实例化内部类对象:

要访问实例内部类中成员,必须要创建实例内部类的对象,而实例内部类定义位置与外部类成员定义位置相同,因此创建实例内部类对象时必须借助外部类创建内部类对象。

OutClass.InnerClass innerClass1 = new OutClass().new InnerClass();
//这里的new OutClass()是匿名对象,一般每次使用的时候,都要new一个对象。

我们也可以先将外部类对象先创建出来,然后再通过外部类对象来创建内部类对象

OutClass outClass = new OutClass();
OutClass.InnerClass innerClass2 = outClass.new InnerClass();

语法:外部类名.内部类名 变量名 = 外部类对象的引用.new 内部类名();

实例内部类当中不能定义静态的成员:

为什么会报错呢?其实不难理解,内部类InnerClass需要依赖于 外部类对象 才能够进行实例化,而static是不依赖于对象的,这不就冲突了么。

同理,实例内部类也不可以有静态方法:

如果非要在内部类定义静态成员的话,只能是编译的时候就要确定的常量变量(即:必须是 static finna修饰的成员):

static final 可以在内部类里面使用,但不能在方法里面使用。

如果在实例内部类中的成员变量和外部类成员变量重名了,就会优先使用实例内部类自己的:

 如何在同名的情况下去调用外部类的data呢?通过外部类类名.this.data即可

 代码示例进行知识总结:

public class OutClass {
    private int a;
    static int b;
    int c;
    public void methodA(){
        a = 10;
        System.out.println(a);
    }
    public static void methodB(){
        System.out.println(b);
    }
    // 成员内部类:未被static修饰
    class InnerClass{
        int c;
        public void methodInner(){
// 在内部类中可以直接访问外部类中:任意访问限定符修饰的成员
            a = 100;
            b =200;
            methodA();
            methodB();
// 如果外部类和内部类中具有相同名称成员时,优先访问的是内部类自己的
            c = 300;
            System.out.println(c);
// 如果要访问外部类同名成员时候,必须:外部类名称.this.同名成员名字
            OutClass.this.c = 400;
            System.out.println(OutClass.this.c);
        }
    }
    public static void main(String[] args) {
// 外部类:对象创建 以及 成员访问
        OutClass outClass = new OutClass();
        System.out.println(outClass.a);
        System.out.println(OutClass.b);
        System.out.println(outClass.c);
        outClass.methodA();
        outClass.methodB();
        System.out.println("=============内部类的访问=============");
// 要访问普通内部类中成员,必须要创建普通内部类的对象
// 而普通内部类定义与外部类成员定义位置相同,因此创建普通内部类对象时必须借助外部类创建内部类对象
        OutClass.InnerClass innerClass1 = new OutClass().new InnerClass();
// 上述语法比较怪异,也可以先将外部类对象先创建出来,然后再创建内部类对象
        OutClass.InnerClass innerClass2 = outClass.new InnerClass();
        innerClass2.methodInner();
    }
}

注意事项
  • 外部类中的任何成员都可以被在实例内部类方法中直接访问
  • 实例内部类所处的成员与外部类成员位置相同,因此也受public、private等访问限定符的约束
  • 在实例内部类方法中访问同名的成员时,优先访问自己的,如果要访问外部类同名的成员,必须:外部类名称.this.同名成员 来访问
  • 实例类对象必须在先有外部类对象前提下才能创建
  • 实例内部类的非静态方法中包含了一个指向外部类对象的引用,因此才可以访问外部的任何成员
  • 外部类中,不能直接访问实例内部类中的成员。

9.3.3 静态内部类

被static修饰的内部成员类称为静态内部类

创建一个静态内部类:

class OuterClass2 {
    public int data1 = 10;
    private int data2 = 20;
    public static int data3=30;
    static class InnerClass {
        //这个类就叫做静态内部类
        public int data4 = 40;
        private int data5 = 50;
        public static int data6=60;
    }
}

实例化静态内部类:

 OuterClass2.InnerClass innerClass =new OuterClass2.InnerClass();

外部类名.静态内部类名 变量 = new 外部类名.静态内部类名

在静态内部类当中,直接的访问方式只能访问外部类的静态成员:

如何访问外部类的非静态成员?其实也不难,间接的访问即可,也就是提供外部类对象:

去访问方法的时候也是一样的,这里就不再赘述了

如何访问静态内部类的成员:

示例代码进行总结:

public class OutClass {
    private int a;
    static int b;
    public void methodA(){
        a = 10;
        System.out.println(a);
    }
    public static void methodB(){
        System.out.println(b);
    }
    // 静态内部类:被static修饰的成员内部类
    static class InnerClass{
        public void methodInner(){
// 在内部类中只能访问外部类的静态成员
// a = 100; // 编译失败,因为a不是类成员变量
            b =200;
// methodA(); // 编译失败,因为methodB()不是类成员方法
            methodB();
        }
    }
    public static void main(String[] args) {
// 静态内部类对象创建 & 成员访问
        OutClass.InnerClass innerClass = new OutClass.InnerClass();
        innerClass.methodInner();
    }
}
​​​​​​​
注意事项
  • 在静态内部类中只能访问外部类中的静态成员
  • 创建静态内部类对象时,不需要先创建外部类对象
  • 成员内部类,经过编译之后会生成独立的字节码文件,命名格式为:外部类名称$内部类名称

9.4 局部内部类

定义在外部类的方法体或者{}中,该种内部类只能在其定义的位置使用,一般使用的非常少,此处简单了解下语法格式。
public class OutClass {
    int a = 10;
    public void method(){
        int b = 10;
// 局部内部类:定义在方法体内部
// 不能被public、static等访问限定符修饰
        class InnerClass{
            public void methodInnerClass(){
                System.out.println(a);
                System.out.println(b);
            }
        }
// 只能在该方法体内部使用,其他位置都不能用
        InnerClass innerClass = new InnerClass();
        innerClass.methodInnerClass();
    }
    public static void main(String[] args) {
// OutClass.InnerClass innerClass = null; 编译失败
    }
}
【注意事项】
  • 局部内部类只能在所定义的方法体内部使用
  • 不能被public、static等修饰符修饰
  • 编译器也有自己独立的字节码文件,命名格式:外部类名字$x内部类名字.class,x是一个整数。
  • 几乎不会使用,可以说狗都不用。

9.5 匿名内部类

之后讲到接口会给大家进行详细的介绍

End!

能坚持看完非常厉害了,加油。

如果再也不能见到你,也祝你早安午安还有晚安。

⭐⭐⭐⭐⭐⭐⭐ 本章完,愿有所收获,一路长虹,祝你也祝我。⭐⭐⭐⭐⭐⭐⭐

猜你喜欢

转载自blog.csdn.net/qq_31713577/article/details/124249032
今日推荐