目录
前言
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
这章我们就要进入类和对象的学习了,这和前面的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
引用作为参数传递给方法。如果this
是null
,那么就没有实例来调用方法。
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 常见的包
- java.lang 系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflflect java 反射编程包。
- java.net 进行网络编程开发包。
- java.sql 进行数据库开发的支持包。
- java.util 是java提供的工具程序包。(集合类等) 非常重要
- 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类实例化三个对象s1、s2、s3:
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!
能坚持看完非常厉害了,加油。
如果再也不能见到你,也祝你早安午安还有晚安。
⭐⭐⭐⭐⭐⭐⭐ 本章完,愿有所收获,一路长虹,祝你也祝我。⭐⭐⭐⭐⭐⭐⭐