Bootstrap

Java面向对象编程

Java面向对象编程

文章目录


前言:面向对象的三条主线

  • Java类及类的成员:(重点)成员变量、方法、构造器;(熟悉)代码块、内部类
    类的成员之间是可以相互包含的
  • 面向对象的特征:封装、继承、多态、(抽象) ——类及其成员的权限、子类继承父类的属性/方法、子类对象的多态性/方法重写
  • 其他关键字的使用:this、super、package、import、static、final、abstract等
  • 特殊的引用类型(可以理解为与类并列地位):接口interface、枚举enum、注解@interface、记录record

变量:成员变量、局部变量(方法等结构体中定义的)
成员变量:实例变量(属性、field/字段/域,属于对象的,非静态)、类变量(类属性,属于类的,静态)
方法:实例方法(属于对象的,非静态)、类方法(属于类的,静态)

数据类型:引用数据类型(数组[]、类class、接口interface、枚举enum、注解@interface、记录)

程序语言设计:提高代码的复用性(方法、面向对象的特性、定义的关键字等等可以实现)

一、面向对象编程概述

1.1 程序设计思路

1、定义:是软件开发中的一类编程风格、开发范式。

  • 面向过程的程序设计思想(Process-Oriented Programming, POP):关注操作数据的步骤,某个过程的实现代码重复出现,将这个过程抽取为一个函数,简化冗余代码。
    比如C语言:以函数为组织单位。是一种“ 执行者思维 ”,适合解决简单问题。扩展能力差、后期维护难度较大。
  • 面向对象OOP:关注类(属性/行为),在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。如Java、C#、C++、Python、Ruby和PHP等
    比如Java:是一种“ 设计者思维 ”,适合解决复杂问题。代码扩展性强、可维护性高。

面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是,具体到实现部分的微观操 作(就是一个个方法),仍然需要面向过程的思路去处理。 当需求单一,或者简单时,我们一步步去操作没问题,并且效率也挺高。可随着需求的更改,功能的增多,发现需要面对每一个步骤很麻烦了,这时就开始思索,能不能 把这些步骤和功能进行封装,封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候,找到对应的类就可以了。这就是面向对象的思想。

1.2 Java语言的基本元素:类和对象

1、类和对象概述

  • 类:具有相同特征的事物的抽象描述,是 抽象的 、概念上的定义。
  • 对象:实际存在的该类事物的每个个体 ,是具体的 ,因而也称为实例(instance) 。

2、类的成员:(重点)属性/方法/构造/(熟悉)内部类/代码块
设计类:本质就是设计类的成员
类的成员:类,是一组相关 属性 和 行为 的集合(基本成员:属性、行为——对应类中的成员变量(非静态)、方法)

  • 属性:即类中的成员变量(非静态),表示该类事物的状态信息。
    非静态的成员变量<=>属性<=> Field
  • 行为:即类中的成员方法,表示该类事物要做什么操作,或者基于事物的状态能做什么。对应
    (成员)方法 <=>函数 <=> Method

3、 使用面向对象步骤:定义/创建/调用

  • 步骤1:定义类:使用class关键字,并设计类的北部成员(属性、方法)
  • 步骤2:实例化类(又称创建类的对象/实例):使用new关键字(使用new创建的实体对象都在堆中)。
    注意还有一种匿名对象:以不定义对象的句柄,而直接调用这个对象的方法。用途:只用一次/作为方法实参。
  • 步骤3:通过对象调用其内部声明的属性(实例变量/非静态的成员变量)或方法完成相关功能:对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。
    访问对象成员语法格式:" 对象名.属性(这里的说法比较准确,而非成员变量) " 、 " 对象名.方法 "
    注意:静态成员可以直接通过类来调用,无需创建对象

使用面向对象的步骤:

//步骤1:定义类
[修饰符] class 类名{
    属性声明;
    方法声明;
}
//步骤2:创建对象
//方式1:给创建的对象命名
//把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了
类名 对象名 = new 类名();
//方式2:
new 类名()//也称为匿名对象
//调用方法
new Person().shout();

图示理解
Java中的类和对象

1.3 对象的内存解析(JVM)

先理解引用数据类型:引用数据类型的变量中存储的是对象的地址(它指向堆中对象的首地址堆中)。引用类型的对象名存储堆中的首地址(其格式为:“类型@对象的hashCode值"。方法执行完,自动释放),基本数据类型的变量名存储具体的值。
引用类型理解:虚拟机栈存储对象名(对象引用),该变量名的值是堆中的首地址值。真正的实体存放在堆中。
创建一个类的多个对象(比如p1、p2),则每个对象都拥有当前类的一套"副本"(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。 但是如果p1=p2,此时他们指向堆中的同一个地址,p2改变时,p1也会随之改变。

为了提高运算效率,就对JVM空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
1、JVM内存结构划分:虚拟机栈、本地方法栈、堆、方法区、程序计数器
HotSpot Java虚拟机的架构图。JVM理解为一种规范,HotSpot JVM是实现。关注运行时数据区部分(Runtime Data Area)主要区域有:

  • 堆(Heap) :new出来的结构——①数组实体、对象实体;②对象的属性(类的非静态成员变量)。Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆。
  • 虚拟机栈(Stack) :以栈帧为基本单位,有入栈和出栈的操作;每个栈帧操作对应一个方法的执行。每个栈帧中都拥有:
    • 局部变量表:存放了编译期可知的各种数据类型、对象引用、
    • 操作数栈:存放方法执行过程中产生的中间计算结果,临时变量
    • 动态链接:一个方法需要调用其他方法的场景,链接太多会出现StackOverFlowError异常,递归太多会出现OutOfMemoryError异常
    • 方法返回地址:return、抛出异常
  • 本地方法栈:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  • 方法区(Method Area)——抽象概念/规范上(具体实现可能不同) :存储已被虚拟机加载的Class文件中的相关信息、③静态变量、④即时编译器编译后的代码缓存(即JIT代码缓存)等数据。
    • 补充:当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。
      Class文件中的相关信息:①有类的版本、字段、方法、接口等描述信息,还有用于存放编译器生成的各种字面量(Literal,包括整数、浮点数和字符串字面量)、符号引用(Symbolic Reference,包括符号引用–类 / 字段 / 方法 / 接口方法)的②常量池表(Constant Pool Table)
    • 运行时常量池:编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的常量池表,常量池表会在类加载后存放到方法区的运行时常量池中,受到方法区内存的限制。
    • 字符串常量池:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

明确永久代、元空间是方法区的具体实现,统一叫方法区即可,不同的是里面放的内容不一样了
永久代:JDK1.7及以前:JDK6中包含方法区规范定义的所有信息,但是在JDK1.7将字符串常量池、静态变量永久代中移出到中(原因:永久代实现的GC回收效率低,只有在Full GC才执行)
元空间:JDK1.8及之后:采用本地内存实现方法区,完全废弃永久代,将永久代中剩余内容(主要是类信息)全部移到元空间中

总结:只要是局部变量就在栈中,只要是成员变量(非静态、也称属性/实例变量)就在堆中!不管java成员之间如何相互定义、引用。即如果是对象中的局部变量也是在栈中。
JDK7/8的JVM内存空间
关于方法区:不同实现的分配
JDK6、JDK7的永久代使用的还是JVM内存
JDK8的元空间与JDK7的分配一致,不同的是使用本地内存
关于方法区:不同实现的分配
JVM的架构简图
JVM架构-简图

//首先进栈的是main方法
class Student{
	static int num = 0;//静态的成员变量
	int number;//非静态的成员变量,属性
	int state;
	//slee表示方法
	void sleep(int i){//i表示局部变量(基本数据类型)
	}
}
class shixian{
	//首先进栈的main方法帧,只有这样才能实现一系列的调用、实例化等等!
	public static void main(string[] args){
		static int num = 0;//静态的局部变量
		int num1 = 2;//局部变量——基本数据类型
		Student[] stus = new Student[3];//局部变量:stus为引用数据类型——数组类型
		for (inti=0;i< stus.length; i++){//i表示局部变量,stus.length表示属性
			stusli] = new Student();//stus数组存储堆的数组的首地址,每个地址中存储的又是Students对象在堆中的地址
		}
		stus[0].number = 1;//对象的属性,又称作实例变量!
		stus[0].state = 5;
		stus[0].sleep(6);//引用方法,调用对象的方法
	}
}

2、内存举例

  • 每一个方法的调用对应的是一个栈帧
  • 程序开始:main方法进栈,其相关的局部变量、对象引用(指向对象对内存地址)进栈
  • 对象调用方法:对象的方法进栈
  • 对象赋值对象:p3=p1,得到p1和p3指向同一个堆内存的实体对象首地址
  • 对象数组内存解析:数组的元素类型是对象,首先栈中存数组对象引用,数组元素中存实体对象的地址
    在这里插入图片描述
    在这里插入图片描述

总结:

  • 堆:凡是new出来的结构(对象、数组)都放在堆空间中。 对象的属性存放在堆空间中。
  • 创建一个类的多个对象(比如p1、p2),每个对象在堆空间有一个对象实体,每个对象实体中保存着一份类的属性。当通过一个对象修改其属性时,不会影响其它对象此属性的值。
  • 当声明一个新的变量使用现有的对象进行赋值时(比如p3 = p1),此时并没有在堆空间中创 建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时, 会影响另外一个对象对此属性的调用。

二、类的成员1—成员变量

静态的成员变量又称属性、实例变量、field

2.1 “变量”定义&分类

1、变量分类:变量必须先声明后赋值再使用,作用域内有效
注意:这里是变量的分类,不是成员变量!

  • 数据类型不同:基本数据类型(8种)、引用数据类型(数组[]、类class、接口interface、枚举enum、注解@interface、记录)
  • 变量在类中声明的位置不同:
    • 成员变量:实例变量(属性,属于对象的,非静态)、类变量(类属性,属于类,静态)
    • 局部变量(方法/构造器/内部类/代码块等内或其形参):也分为静态和非静态的。
  • static修饰:静态变量、非静态变量

总结:以上三种分类可以组合

2.2 实例变量(属性)VS局部变量(主要是非static)

1、实例变量(属性)VS局部变量

  • 相同点
    • 必须先声明后使用,且声明格式相同:数据类型 变量名 = 初始化值
    • 都有其对应的作用域,只在作用域内是有效的。
  • 不同点
    • 声明位置&内存位置:定义在类中方法外,随着对象的创建存储在堆中;方法体或其形参列表/块中,随着方法的调用存储在栈中。
    • 作用域&生命周期:实例变量本类中直接调用,其他类中"对象名.属性",而局部变量超出作用域就不生效了。
      • 与对象声明周期一致,对象创建-GC回收,回收对象其实就是回收对象实体的这些属性。每一个对象的实例变量都是独立的。
      • 方法栈帧入栈→局部变量在栈中分配→方法栈帧出栈,局部变量消亡。即从方法调用到方法执行结束。每一次方法调用都是独立。
    • 修饰符:public,protected,private,final,volatile,transient等;final,因为局部变量作用域就是方法,如果声明像前面的那种也没意思,外面根本也不能访问!
    • 默认值:属性有默认值(不显示赋值也有值);局部变量没有默认值,必须手动初始化。其中的形参比较特殊,依靠实参给它初始化!

2、实例变量(属性)的默认初始化赋值

  • 什么时候给实例变量赋默认值?
    当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。
  • 各个类型的对象属性的默认值
    • 整数(int/byte/short):0;long:0L,因为java中整数的默认类型是int型
    • 浮点型:float:0.0F,double:0.0,java中默认的数据类型是double
    • char:0或 ‘\u0000’
    • boolean:false(0)
    • 引用类型:null

2.3 变量的内存分析

只要是局部变量就在栈中,只要是成员变量(非静态、也称属性/实例变量)就在堆中!不管java成员之间如何相互定义、引用。即如果是对象中的局部变量也是在栈中。
变量的内存分析

2.4 类的属性赋值过程(实例变量)

按照变量在类中声明位置的不同可以分为成员变量(类中方法外,又分为类变量、实例变量)、局部变量(方法中,也分为static)。其中实例变量的赋值过程在这里介绍,局部变量(非静态)的赋值叫做值传递,在3.4中介绍。

1、赋值方法:执行顺序:①—②/③—④—⑤/⑥,②③顺序可以调换
———初始化:对象创建时,只能执行一次———————
①默认初始化(可以理解为默认的无参构造器)
②显示初始化:在类中直接给其赋值。
③代码块中初始化
④构造器中初始化(自定义的构造器)
———赋值:对象创建后,可以执行多次———————
⑤"对象.方法"方式,给属性赋值。
⑥通过"对象.属性"赋值。
关于赋值的顺序,显式赋值先于代码块/构造器中的赋值,通过编译后的字节码文件就可以看到java底层定义的执行逻辑。
代码块先于构造器执行,静态代码块先于非静态的执行。
总结:由于有不同的赋值方法,可以对实例变量设置权限修饰符,控制实例变量的赋值方式。比如只能通过构造器啥的,或者private 属性,然后提供public get、set方法进行调用方法赋值。
具体原因可以参考4.2

2、如何选择赋值方法
显式赋值:适合每个对象的属性值相同的场景
构造器中赋值:比较适合每个对象的属性值不同的场景

三、类的成员2—方法(主要是非static)

方法又称函数、method。方法不调用不执行,每调用一次执行一次(进栈-出栈)
为什么需要方法:比如游戏中人物每次出拳就是一个重复的动作,可以单独抽取出来成为一个方法!
方法:实例方法(属于对象,非静态)、类方法(属于类,静态)

3.1 方法介绍&理解

1、方法定义:方法 是类或对象行为特征的抽象,用来完成某个功能操作。也称为函数或过程 。将功能封装为方法是为了可以实现代码重用,减少冗余,简化代码。必须定义在类中,不能独立存在!在C/js语言中是可以独立存在的
2、方法构成:方法头 + 方法体

  • 方法头:也称方法签名
    • [修饰符]:权限修饰符:public、缺省、protected、private;static(是否静态)、abstract(是否抽象)、native、final、synchronized等。
    • 返回值类型: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。分类:void、有返回值(任何类型,与return搭配)
    • 方法名:属于标识符,命名遵循标识符命名规则和规范。
    • [形参列表]:表示完成方法体功能时需要外部提供的数据列表。0/多个,形参需指定数据类型和参数名,()不能省,逗号分隔!
    • [throws 异常列表]:在异常处理中理解。
  • 方法体:方法被调用后要执行的代码(方法功能),必须有{}括起来。方法体必须要有!
//一个完整的方法 = 方法头 + 方法
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表(受检查异常)]{ 
	方法体的功能代码 
}

3.2 方法的返回值&调用方法

1、方法返回值return语句实现:

  • void时,return后不能跟返回值,可以省略
  • 不是void时,return 返回值,一定要有。

return在方法中的作用:结束一个方法;结束一个方法的同时,返回数据给方法的调用者
注意点:在return关键字的直接后面不能声明执行语句,否则会报错Unreachable code!
怎么可以结束循环?
条件不满足;return语句(直接把循环所在的方法都结束了);break。注意continue不行,它只是结束本次循环!
2、方法的调用:

  • 静态方法:又称类方法,在同一个类中直接通过方法名调用,不同的类中只能通过类调用,所有对象共享这一个方法。
  • 实例化方法:在同一个类中直接通过方法名调用,不同类中,实例化对象调用实例方法

两种方法调用返回值情况:

  • 实例方法无返回值:无需定义变量去接收方法的结果
  • 实例方法有返回值:定义变量来接收实例方法的返回值,如果没有接收实例方法的返回值那么会导致结果丢失,但是实例方法会正常执行。

3、方法使用的注意点

  • 必须先声明后使用,且方法必须定义在类的内部
  • 调用一次就执行一次,不调用不执行。
  • 方法中可以调用类中的方法或属性,不可以在方法内部定义方法。但是方法级别不一样的时候可以,比如static的main方法中定义方法了!

3.3 方法调用内存分析

每一个方法属于一个栈帧,栈帧是栈的基本结构!

  • 方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。
  • 方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。
  • 当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。
  • 栈结构:先进后出,后进先出。

注意:调用太多的方法(比如递归太多次时),可能会导致栈中溢出

  • 方法调用一个从栈中出去一个,这里为了形象一点还是放在里面的。如果在方法中调用方法不是的,比如main方法一直在里面,在main方法中调用了很多其他的方法。递归也不是的。
  • 实例方法中的属性可以通过"对象名.属性操作",也可以在对象类中定义方法来操作相关的属性,那么当调用该方法时,也可以实现对属性的操作。比如:(1)创建Person类的对象,设置该对象的name、age和sex属性,调用study方法,输出字符串“studying”,调用showAge()方法显示age值,调用addAge()方法给对象的age属性值增加2岁。 (2)创建第二个对象,执行上述操作,体会同一个类的不同对象之间的关系
    方法调用内存分析

—————方法的一些特性———————

3.3 方法重载&可变个数形参

1、方法重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。满足这样特征的多个方法,彼此之间构成方法的重载。
“两同一不同”:同一个类、方法名,参数列表不同(个数,类型,顺序不一样也算)。
注意父子类中也可以,因为子类继承父类的方法,那么代表这个方法也在子类中了,因此在子类中可以对这个方法重载。但是要注意,同一个类中不能定义同方法名同参数列表的,权限修饰符不一样也不行,但是在父子类中可以,在子类虽然有父类的方法,但是不能访问,在子类定义一个同方法名同参数列表的也可以。
重载方法调用:JVM通过方法的参数列表,调用匹配的方法。

  • 先找个数、类型最匹配的
  • 再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错。

编译器如何确定调用某个具体的方法呢?

  • 先通过方法名确定一波重载的方法,进而通过不同的形参列表,确定具体的某一个方法。
    如何判断两个方法是相同的?
    方法名相同,且形参列表相同(参数个数、类型),与形参名无关,与修饰符、返回值类型无关。

判断与void show(int a,char b,double c){} 构成重载的有:

a)void show(int x,char y,double z){}     // no
b)int show(int a,double c,char b){}       // yes
c) void show(int a,double c,char b){}     // yes
d) boolean show(int c,char b){}           // yes
e) void show(double c){}                  // yes
f) double show(int x,char y,double z){}  // no
g) void shows(){double c}        // no        

方法重载的应用实例
下面的println()虽然方法名一致,但是传入的参数类型不一样,调用的重载方法也是不一样的,所以有的输出地址值,有的输出元素

  • void java.io.PrintStream.println(Object x):在终端输出Object类型
  • void java.io.PrintStream.println(char[] x):在终端输出characters字符
    除了上面这些println还有很多其他重载方法!
int[] arr1 = new int[]{3, 2, 5, 1, 6};
System.out.println(arr1);//地址值
char[] arr2 = new char[]{'a','b','c'};
System.out.println(arr2);//abc
boolean[] arr3 = new boolean[]{false, true, true};
System.out.println(arr3);//地址值

2、可变个数形参的方法(JDK5.0新特性)
本质上,同类型的数组形参=可变个数形参,可以把它当成数组。与数组相比,比以前省事了,不用再new,直接传。
(1)定义:在JDK 5.0 中提供了Varargs(variable number of arguments)机制。即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。
本质上,为了传入多个同一类型的变量,这与数组的功能是一致的,因此再JDK5.0之前都是使用的数组来实现这样的功能的!
(2)格式:主要改变的是形参列表→方法名(参数类型 …参数名)。这与形参使用数组是一致的不同同时使用:方法名(参数类型[] 参数名)

//下面这两种方法是构成重载的
int add(int x, int[] y);//JDK5.0以前:使用数组,传入多个同一类型变量
void add(int x, int ... y);//JDK5.0:可变个数形参

(3)特点:

  • 可变形参个数:0个,1个或多个;需要放在形参声明的最后且只能声明一个,如果放前面的话,编译器就分不清了。
  • 可变个数形参的方法与同名的方法之间,彼此构成重载
  • 与使用同类型的数组参数的方法构成重载(同一个类,同一个方法名),它们两不能同时使用,否则就会报错!因此在调用可变个数形参的方法是,可以传入同类型的数组。

(4)遍历可变个数形参
把它当成数组来遍历,与数组的遍历方法一样!
(5)应用常见示例
sql语句中传入的参数的个数,每次可能不一样。

3.4 方法的值传递机制(方法局部变量的赋值过程)(重点)

3.4.1 值传递介绍

值传递:存的是什么就传的是什么!
Java里方法的参数传递方式只有一种:值传递(不是引用传递)。

1、值传递的原则

  • 如果是基本数据类型的变量,则将此变量保存的数据值传递出去
  • 如果是引用数据类型的变量,则将此变量保存的地址值传递出去,他们两指向同一个堆内存地址。

总结:因此对于引用类型,如对象、数组等等,实参是这些类型的时候,把堆当中的地址值传给方法形参,在方法对于他们的实体做一些改变时是会影响到他们的实体属性值的,只不过地址值没有改变而已!
举例:将数组传递给方法形参,在方法中对数组进行排序,是可以实现数组排序的!
对于方法的参数有两种:方法内的局部变量、方法定义的形参。
2、发生值传递的情形

  • 对于方法内声明的局部变量来说,如果出现赋值操作
  • 实参传递给形参
    形参(formal parameter):在定义方法时,方法名后面括号()中声明的变量称为形式参数,简称形参。
    实参(actual parameter):在调用方法时,方法名后面括号()中的使用的值/变量/表达式称为实际参数,简称实参。
3.4.2 内存分析

1、形参是基本数据类型:传的是数据值

交换两个整型变量的值

  • 第一种方式:直接操作,设置temp→代码实现:int temp = m; m = n; n = temp;
  • 第二种方式:定义方法,将这两个实参传递给方法的形参,在方法中进行交换→这样是不行的,见内存分析!
//第二种方式:定义方法
public class Test {
	public static void main(String[] args) {
		int m = 10;
		int n = 20;
		System.out.println("m = " + m + ", n = " + n);
		Test test = new Test();
		test.swap(m, n);
		System.out.println("m = " + m + ", n = " + n);//结果:m=10,n=20
	}
	//交换m和n的值,但是没有返回值,正确的做法应该是设置返回值达到交换的效果!
	public void swap(int m,int n){
		int temp = m;
		m = n;
		n = temp;
	}
}

形参是基本数据类型的值传递
2、形参是引用数据类型:传的是地址值

和上面一样定义一个交换的方法,只不过这个方法的形参类型定义为引用数据类型
代码中的data是自定义的一个类: class Data{int m;int n;}

public class Test {
    public static void main(String[] args) {
        Data d1 = new Data();
        d1.m = 10;
        d1.n = 20;
        System.out.println("m = " + d1.m + ", n = " + d1.n);
        //实现 换序
        Test test = new Test();
        test.swap(d1);
        System.out.println("m = " + d1.m + ", n = " + d1.n);//结果:m=20,n=10
    }
    public void swap(Data data){
        int temp = data.m;
        data.m = data.n;
        data.n = temp;
    }
 }

形参是引用类型的值传递

3.4.3 值传递的应用&注意点

0、一般在交换数组交换元素的操作时,定义的方法如:swap(int[] arr, int i, int j) //告诉数组,并且指明索引位置。
1、实参为基本数据类型时,传递给方法形参,它本身的值不会改变
2、数组是引用类型,把它的地址值传递给方法形参,在方法中对地址值指向的实体操作时,由于它们指向同一个地址实体,因此原来数组的指向实体是同一个所以发生改变
3、将对象作为参数传递给方法,指向同一个堆内存中的地址
4、str为方法形参,当在方法中str.equals(“sd”),调用该方法并传值时,如果传递的是null,那么就会出现空指针异常NullPointerException。解决办法:“sd”.equals(str);或者判断一下这种情况。
5、比较难的方法调用+值传递的内存分析

JVM内存分析

  • 栈:每个方法在调用时,都会有以栈帧的方法压入栈中。栈帧中保存了当前方法中声明的变量:方法内声明的,形参
  • 堆:存放new出来的"东西":对象(成员变量在对象中)、数组实体(数组元素)。
  • 注意:变量前如果声明有类型,那么这就是一个新的刚要定义的变量。如果变量前没有声明类型,那就说明此变量在之前已经声明过。
public class TransferTest3 {
	public static void main(String args[]) {
		TransferTest3 test = new TransferTest3();
		test.first();
	}
	public void first() {
		int i = 5;
		Value v = new Value();
		v.i = 25;
		second(v, i);
		System.out.println(v.i);
	}
	public void second(Value v, int i) {
		i = 0;
		v.i = 20;
		Value val = new Value();
		v = val;
		System.out.println(v.i + " " + i);
	}
}
class Value {
	int i = 15;
}

值传递内存解析
6、值传递真题
值传递真题

//法一:
public static void method(int a, int b) {
	// 在不改变原本题目的前提下,如何写这个函数才能在main函数中输出a=100,b=200? 
	a = a * 10;
	b = b * 20;
	System.out.println(a);
	System.out.println(b);
	System.exit(0);//直接退出
}
//法二:重写println方法
public static void method(int a, int b) {
	PrintStream ps = new PrintStream(System.out) {
	    @Override
	    public void println(String x) {
	        if ("a=10".equals(x)) {
	            x = "a=100";
	        } else if ("b=10".equals(x)) {
	            x = "b=200";
	        }
	        super.println(x);
	    }
	};
	System.setOut(ps);
}

3.5 递归方法

3.5.1 递归介绍&理解

经典的例子:从前有座上,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,…
递归方法——可以用循环来代替实现。
1、递归方法定义:方法自己调用自己的现象就称为递归。注意:递归太多可能会导致栈"溢出",调用了太多的方法帧在栈中都没有出去!
要有递归出口,且递归方法一般是有返回值的。
2、递归分类

  • 直接递归:方法自身调用自己
  • 间接递归:为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。

3、递归的特点

  • 递归方法包含了一种隐式的循环。
  • 递归方法会重复执行某段代码,但这种重复执行无须循环控制。
  • 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出StackOverflowError

4、最后关于递归说明

  • 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢得多,所以在使用递归时要慎重。
  • 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存,考虑使用循环迭代
3.5.1 递归方法的内存分析&例题应用(fib/honi)

(1)计算1~n的和

法一:递归方法;法二:用for循环

public class RecursionDemo{
    public static void main(String[] args) {
        RecursionDemo demo = new RecursionDemo();
        int n = 5;
        int sum = demo.getSum(n);
        System.out.println(x);
    }
    //定义递归方法
    public int getSum(int num){
        if(num == 1){
            return 1;
        }else{
            return getSum(num-1) + num;
        }
    }
}

递归方法的内存分析

(2)斐波那契:从第三个数开始,一个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足:
f(n) = f(n-2) + f(n-1);其中f(1)=1;f(2)=1。下面直接给出函数实现

斐波那契的实现:①递归;②循环

  • 递归:要有递归出口,递归出口一定要结束方法。
  • 动态规划:使用dp数组记录每次的值,要有初状态、子问题之间的联系!
  • 由于这题不用记录所有状态的,因此可以设置三个变量分别记录i-2、i-1、i对应的斐波那契结果
//法一:使用递归实现
public int fib(int num){
	if(num < 0){
	    return 1;
	}
	if(num == 1 || num == 2){
	    return 1;
	}
	return fib(num-1) + fib(num-2);
}
//法一:不使用递归实现————使用动态规划
public int fibValue(int num){
    if(num < 1){
        return 1;
    }
    
    int[] dp = new int[num];
    dp[0] = 1;
    dp[1] = 1;
    for(int i = 2; i < num; i++){
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[num-1];
}
//法三:循环实现
int fValue(int n) {//计算斐波那契数列第n个值是多少
	if (n < 1) {//负数是返回特殊值1,表示不计算负数情况
	    return 1;
	}
	if (n == 1 || n == 2) {
	    return 1;
	}
	//从第三个数开始,  等于 前两个整数相加
	int beforeBefore = 1; //相当于n=1时的值
	int before = 1;//相当于n=2时的值
	int current = beforeBefore + before; //相当于n=3的值
	//再完后
	for (int i = 4; i <= n; i++) {
	    beforeBefore = before;
	    before = current;
	    current = beforeBefore + before;
 	}
 	return current;
 }

(3)Honi汉诺塔
思路:将n和盘子看成两部分,最下面的第n个盘子和上面n-1组合起来的盘子

public void honi(int num, char first, char mid, char last){
	//既然是递归方法,那么就要有方法的递归出口
	if(num == 1){
	    System.out.println("将第一个盘子从柱子"+fromRod+"移动到柱子"+toRod);
	    //一定要有递归出口,结束递归方法的调用
	    return;
	}
	//将所有的盘子视为两部分:最下面一个n,和最下面的上面一个n-1。
	honi(num-1,fromRod,tempRod,toRod);
	System.out.println("将第"+num+"个盘子从柱子"+fromRod+"移动到柱子"+toRod);
	honi(num-1,tempRod,toRod,fromRod);
}

(4)扩展:走台阶问题——假如有10阶楼梯,小朋友每次只能向上走1阶或2阶,请问一共有多少种不同的走法呢?
思路:找找规律:1、2、3、4、5…分别对应的走法:1、2、3、4、7…得到关系式f(i) = f(i-1)+f(i-2),其中f(1)=1,f(2)=2。

四、类的成员3—构造器/构造方法(没有static)

即Person p = new Person()的本质,使用的就是构造器。凡是类都有构造器,就算abstract类也有构造器。
如果没有自定义,那么系统会提供一个默认的无参构造器,源代码文件中是没有的,但是编译后的字节码文件(可以看比较直观的反编译后的字节码文件)有!

解释:比如创建每个对象都必须完成的动作在构造器中定义,就没必要再一个一个地告诉他们了。
注意:构造器的作用只是用来初始化,必须要搭配new才能创建对象→子父类&本类之间的构造器调用不会创建多个对象!
1、定义:搭配new关键字,创建类的对象(注意在构造器中调用其他构造器,没有搭配new是不会创建对象的),并在创建对象的同时为对象的相关属性(实例变量)赋值。比如Person p = new Person(“Peter”,15) ;
2、构造器的语法格式:与方法的定义差不多

构造器定义的语法格式,注意点如下:

  • 构造器名=类名
  • 注意没有返回值(不能有return),所以不需要返回值类型,包括void。
  • 只能是权限修饰符,不能被其他任何修饰。比如,不能被static、final、synchronized、abstract、native修饰
[权限修饰符] 构造器名(参数列表){//,
	实例初始化代码
}

3、构造器的特点

  • 在类中,至少会存在一个构造器。
  • 当没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同(类的权限修饰符只能是public或者缺省,但是构造器的修饰都可以用)。但是当我们显示的定义类的构造器以后,系统就不再提供没人的无参构造器了(就没有了),因此在显示声明时,最好再加上无参的构造器。
  • 一个类中可以声明多个构造器,是可以重载的。(参数列表必须要不一样)

为什么创建对象"Person p = new Person()"后该对象的属性(实例变量)是默认值?
因为系统会默认提供一个无参构造器,而构造器的作用就是new对象,并且在new对象的时候为实例变量赋值,没有显示赋值时,赋的就是默认值。
如果创建一个对象?
最底层都是使用类中的构造器实现创建对象的,构造器的调用方法就是new的。
4、构造器的应用:Scanner sc = new Scanner(System.in);//源码中定义了不同的Scanner构造器,在一创建对象就要做的事

五、类的成员4—代码块/初始化块(static两种一起)

1、代码块的使用场景:成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值。
作用:也叫做初始化块,用于对Java类或对象进行初始化
分类:关于静态变量初始化的方式:静态代码块中;在静态变量声明后直接赋值

  • 静态代码块static修饰:静态变量初始化
    • 可以有输出语句
    • 对类的属性、类的声明进行初始化操作。但是不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
    • 静态代码块随着类的加载而加载,且只执行一次。
  • 非静态代码块:和构造器一样,也是用于实例变量的初始化等操作。因此如果多个重载构造器有公共代码且先于构造器其他代码执行可以使用非静态代码块。
  • 可以有输出语句。
  • 对类的属性、类的声明进行初始化操作。不仅可以调用非静态的结构,还可以调用静态的变量或方法。
  • 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。

先执行静态的后执行非静态的,与定义的顺序无关,都可以有多个,同类型的代码块按照定义的顺序执行。都先于构造器执行。
静态代码块内部只能使用静态结构,非静态代码块结构都能使用。【依据:生命周期】
具体使用:提取构造器公共代码(先于构造器执行)到非静态代码块;还可以用于获取数据源。

静态代码块的定义格式:

static{
	//代码块
}
{
	//代码块
}

代码块的应用:想要初始化静态属性赋值

private static DataSource dataSource = null;

static{
	InputStream is = null;
	try {
		is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
		Properties pros = new Properties();
		pros.load(is);
		//调用BasicDataSourceFactory的静态方法,获取数据源。
		dataSource = BasicDataSourceFactory.createDataSource(pros);
	} catch (Exception e) {
		e.printStackTrace();
	}finally{
		if(is != null){
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}		
		}		
	}		
}

六、类的成员5—内部类(static两种一起)

6.1 内部类的介绍&理解

1、内部类的理解:将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass),类B则称为外部类(OuterClass)
作用:一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,并且这个B只为A提供服务,不能在其他地方单独使用。遵循"高内聚,低耦合"开发原则
2、内部类的使用举例:
Thread类内部声明了State类,表示线程的生命周期。
HashMap类中声明了Node类,表示封装的key和value,他们本质上是一个对象的两个属性

3、内部类的分类(参考变量的分类):有各自的作用域
成员内部类:声明在外部类里面
局部内部类:声明在方法内、构造器内、代码块内的内部类
在这里插入图片描述

6.2 成员内部类

1、成员内部类的理解

  • 作为类的角色
    • 可以有类的五个成员
    • 可以继承父类、实现接口
    • 可以使用final:不可继承性
    • abstract修饰:abstract修饰的类的特点是不能创建对象,可以被其他内部类继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件,局部内部类也是这样,只不过不同的局部内部类可能同名,因此字节码文件按顺序1…n做了一个区分,对于匿名局部内部类直接用1…n区分。
  • 作为类的成员的角色
    • 在内部可以调用外部类的五个成员结构,包括私有
    • 外部类只能用public、缺省修饰,但是内部类还能使用private、protected修饰
    • 可以使用static修饰:表示类的静态内部类,使用场景当想要在外部类的静态成员中使用内部类时。注意此时不能使用外部类中的静态成员,但是内部类里面可以定义静态/非静态结构。
      static特性:静态成员是随着类的的加载而加载,其中不能使用非静态结构,反过来可以,不能修饰类。

定义成员内部类的语法

//外部类的修饰符:public、缺省、final、abstract,不能用abstract修饰
[修饰符] class 外部类{
//修饰符可以是:public、缺省、protected、private、static、abstract(必须被继承,类和对象都不能调用)、final(不可继承)
    [其他修饰符] [static] class 内部类{
    }
}

2、创建成员内部类对象
成员内部类属于外部类的成员之一,类调用静态成员:“类名.静态内部类”;类调用非静态成员:"类对象.非静态内部类"得到的是成员内部类类型。静态依赖于类,非静态依赖于对象,静态/非静态内部类中定义的静态/非静态成员调用也是同样的方法!

创建成员内部类对象:静态、非静态
注意创建非静态内部类的方式:外部类名.非静态内部类名 内部类对象 = 外部类对象.new 非静态内部类名();

//创建静态内部类实例→调用内部类中的非静态方法
外部类名.静态内部类名 内部类对象 = new 外部类名.静态内部类名();
内部类对象.非静态方法();//因为静态方法是通过类调用的
//调用静态内部类中的静态方法
外部类名.静态内部类名.静态方法();

//调用非静态内部类中的非静态方法:先创建外部类对象→创建内部类对象→调用内部类的非静态方法
外部类名 外部类对象 = new 外部类();
外部类名.非静态内部类名 内部类对象 = 外部类对象.new 非静态内部类名();
内部类对象.非静态方法();
//调用非静态内部类中的静态方法
外部类名 外部类对象 = new 外部类();
外部类对象.非静态内部类类名.静态方法();

3、在内部类的非静态成员结构中调用外部类中/内部类中成员变量(静态/非静态),其他结构也是如此!
名字不一样时就直接调用即可,但是当出现外部类的成员变量、内部类的成员变量、内部类的成员方法的形参/局部变量重名时应该怎么调用?
涉及到的有:静态/非静态内部类的实例方法、静态方法;①外部类中的实例变量、②类变量;内部类中的③实例变量、④类变量。静态内部类中不能调用外部类中的静态结构,但是其内部能定义静态/非静态结构!

  • 非静态内部类中的实例方法中调用各个成员变量:①②③④
    • 外部类的成员变量:①"外部类名.this.外部类的实例变量"、②"外部类.外部类的类属性(静态)"
    • 内部类的成员变量:③"this.内部类的实例变量"、④"内部类名.内部类的类属性(静态)"
  • 静态的内部类中的实例方法中调用各个成员变量:②③④,不能调用静态内部类外面的非静态结构①
    • 外部类的实例变量:②"外部类.外部类的类属性(静态)"
    • 内部类的成员变量:③"this.内部类的实例变量"、④"内部类名.内部类的类属性(静态)"
  • 静态的内部类中的静态方法中调用各个成员变量:②④,不能调用该静态方法外面的非静态结构①③
    • 外部类的实例变量:②"外部类.外部类的类属性(静态)"
    • 内部类的成员变量:④"内部类名.内部类的类属性(静态)"

总结:重点关注对于非静态变量①③的调用方式即可,其他的都是基于静态结构中不能调用非静态的性质得到的!

创建内部类对象、同名时不同成员变量的调用

class Outer{
    private static String a = "外部类的静态a";
    private String b = "外部类对象的非静态b";
    class NoStaticInner{
        private static String a = "非静态内部类对象的静态a";
        private String b = "非静态内部类对象的非静态b";
        public void inFun(String a, String b){
            System.out.println("NoStaticInner.inFun");
            System.out.println("Outer.a = " + Outer.a);
            System.out.println("NoStaticInner.a = " + NoStaticInner.a);
            System.out.println("a = " + a);
            System.out.println("Outer.b = " + Outer.this.b);
            System.out.println("NoStaticInner.b = " + this.b);
            System.out.println("b = " + b);
        }
    }
    public NoStaticInner getNoStaticInner(){
        return new NoStaticInner();
    }
}

6.3 局部内部类

1、局部内部类:

  • 非匿名内部类
    • 编译后有自己的独立的字节码文件OuterClass$局部内部类名.class,只不过在内部类名前面冠以外部类名、 $符号、编号。不同的局部内部类可能同名,因此字节码文件按顺序1…n做了一个区分,对于匿名局部内部类直接用1…n区分。
    • 局部内部类如同局部变量一样,有作用域。不能有权限修饰符(没有修饰的必要)
    • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法
  • 匿名内部类:因为这个子类或实现类是一次性的,因此使用匿名局部内部类避免给类命名的问题
    子类在继承/实现的同时,又给他创建对象,一共是完成了这几步

2、局部内部类的语法格式

非静态局部内部类、静态局部内部类

//非匿名内部类
[修饰符] class 外部类{
	[修饰符] 返回值类型 方法名(形参列表){
		[final/abstract] class 内部类{
		}
	}
}
//匿名内部类,它继承父类、实现接口,内部的方法可以直接调用或者赋值给一个变量后再调用一样的效果
new 父类([实参列表]){
	//重写方法
}
//匿名内部接口、抽象类也可以
new 父接口(){
	//重写方法
}

开发中的场景:在定义一个返回值类型为接口的方法中,定义局部内部类是想该接口,最后返回该局部内部类的对象

下面是一个内部类

class Outer{
	
	public Comparable getInstance(){
		//法1:提供接口的实现类的对象
		class MyComparable implements Comparable{
	        @Override
	        public int CompareTo(Object o) {
	            return 0;
	        }
	    }
	    MyComparable c1 = new MyComparable;
	    return c1; 
	    
		//法1:提供接口的实现类的匿名对象
		/**
	    class MyComparable implements Comparable{
	        @Override
	        public int CompareTo(Object o) {
	            return 0;
	        }
	    }
	    //返回这个接口实现类的匿名对象,因为创建对象没有名字,无法再次调用到此对象
	    return new MyComparable; */
	    //法2:实现接口的匿名实现类的对象
	    Comparable c = new Comparable(){//匿名实现类
	    	@Override
	        public int CompareTo(Object o) {
	            return 0;
	        }
	    };
	    return c;
	    //法3:实现接口的匿名实现类的匿名对象
		return new MyComparable(){//接口实现了,但是这个实现类也没有名,因此叫匿名实现类
	    	//直接重写该接口的方法
	    	@Override
	        public int CompareTo(Object o) {
	            return 0;
	        }
	    };//分号不能省
	}
}

练习题:编写一个匿名内部类,它继承Object,并在匿名内部类中,声明一个方法public void test()打印尚硅谷。请编写代码调用这个方法。

package com.atguigu.test01;
public class Test01 {
	public static void main(String[] args) {
		//提供接口的匿名实现类的匿名对象
		new Object(){
			public void test(){
				System.out.println("尚硅谷");
			}
		}.test();//继承父类Object后,创建了一个新的对象,该对象可以调用实现的方法。
		//等价于下面的实现
		/**
		//提供接口的匿名实现类的对象
		Object obj = new Object(){
			public void test(){
				System.out.println("尚硅谷");
			}
		};
		obj.test();
		*/
	}
}

七、面向对象特征1—封装

即类和类的成员的可见性范围!举例生活中的密码,有的人能看,有的不能看。控制类及类的成员的访问者

7.1 封装的介绍&理解

比如使用洗衣机,不用了解洗衣机内部的结构,只需要开关、洗涤模式即可。信息隐藏在内部,外界无法直接操作,只能通过指定方式进行访问和修改。即类之间的访问边界,不能谁都能访问或者谁都不能访问,设计类和其成员的可见性范围!
1、面向对象的开发原则:高内聚,低耦合。

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉。
  • 低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用。

内聚/耦合的概念:内聚,指一个模块内各个元素彼此结合的紧密程度,意味着重用和独立;耦合指一个软件结构内不同模块之间互连程度的度量,意味着多米诺效应牵一发动全身。

2、封装性:所谓封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息。通俗的讲,把该隐藏的隐藏,该暴露的暴露。这就是封装性的设计思想。
好处:类内部发生变化就修改类,对外暴露的接口/方法不变,外部访问着不用知道它的具体变化,只需要会调用即可。

7.2 封装的实现&特性&应用

1、实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。
Java中规定了4中权限修饰符,可以使用权限修饰符修饰类及类的成员,当这些被调用时,体现可见性的大小。
注意类的权限修饰符只能是:public,其他修饰符还可以是:abstract & final。
2、权限修饰符比较及其具体访问范围:本类、|后面都是外部类| 本包、其他包的子类、其他包的非子类

  • public:所有的都可以访问
  • protected:本类、本包、其他包下的子类(继承)
  • 缺省:本类和本包下
  • private:只有本类可以访问

总结:权限修饰符的使用场景,最低级别本类中也可以使用。

  • 外部类可以使用:public、缺省,本包下可以是缺省/public,但是如果跨包只能是public
  • 成员变量、成员方法等可以使用:public、protected、缺省、private
    • 本包下使用:成员的权限修饰符可以是public、protected、缺省
    • 跨包下使用:子类protected、public都可,但是非子类只能是public
    • 跨包使用时:如果类的权限修饰符缺省,就算成员权限修饰符>类的权限修饰符也没有意义,因为根本调不了类!

实际开发时,类中属性一般为private(封装性的体现,提供get、set方法),类一般为public!常量一般会声明为public。

3、封装性的体现

  • 场景1:封装成员变量:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。
  • 场景2:私有化方法:将类中不需要对外暴露的方法设置为private。比如类中内部调用的方法,不希望外部访问调用。
  • 场景3:单例模式中构造器设置为private,避免在类的外部创建实例。

4、封装成员变量的好处

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。

5、实际案例:
在题目中,我们给Animal的对象的legs属性赋值。在实际的常识中,我们知道legs不能赋值为负数的。但是如果直接调用属性legs,是不能加入判断逻辑的。那怎么办呢?

  • 将legs属性私有化(private),禁止在Animal类的外部直接调用此属性提供给legs属性赋值的setLegs()方法,在此方法中加入legs赋值的判断逻辑if(legs>=0&&legs%2 ==日)将此方法暴露出去,使得在Animal类的外部调用此方法,对legs属性赋值。
  • 提供给legs属性获取的getLegs()方法,此方法对外暴露。使得在Animal类的外部还可以调用此属性的值。

6、封装性问题回答

  • 什么是封装性?
    Java中规定了4种权限修饰符,可以使用权限修饰符修饰类及类的成员,当这些被调用时,体现可见性的大小。
  • 如何体现封装性?
    回答那几种场景

7、总结归纳

  • 开发中,一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。
  • 对于final的实例变量,不提供set()方法。(因为会设置为public,后面final关键字的时候讲)
  • 对于static final的成员变量,习惯上使用public修饰。【理解:static本身就是通过类调用的,因此它的可见性范围应该至少是不同类可以调用的级别的】
  • 定义的方法,完成一个功能即可。

八、面向对象特征2—继承extends

继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思

8.1 继承的介绍&理解

补充类的定义:类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
1、继承定义:继承的是实例变量和实例方法,静态的不继承,也不能被继承!

  • 角度一:从上而下:B类继承了A类的所有属性和方法,并增加了一些属性/方法。父类A中的属性和方法,子类B都可以使用。
  • 角度二:从下而上:多个子类B\C中存在相同属性和行为时,将这些内容抽取到单独一个父类A中,那么多个子类B\C中无需再定义这些属性和行为,只需要和抽取出来的类A构成继承关系

总结:为什么子类能够继承到父类的所有属性和方法?是因为在子类构造器中一定会直接或间接调用到父类构造器(默认是无参构造器),显示调用用super。
2、继承的好处:不要仅为了获取其他类中某个功能而去继承!

  • 减少了代码冗余,提高了代码的复用性;且有利于功能的扩展。
  • 类与类之间的关系:is-a,为多态的使用提供了前提。
    • 继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。

3、继承的语法格式:extends关键字

  • B类:子类、派生类(derived class)、SubClass
  • A类:父类、超类、基类(base class)、SuperClass
[修饰符] classA {
	...
}
//类B使用extends继承类A
[修饰符] classB extendsA {
	...
}

注意点

  • 子类和父类的理解,要区别于集合和子集,子类的功能比父类更强大
  • 不要为了继承而继承。在继承之前,判断一下是否有is a的关系。
  • 子类继承父类,其中父类中必须要有无参构造函数。原因:子类中如果没有显示声明构造器,那么在这个默认的构造器中调用的是父类的无参构造器,但是父类中没有无参构造器那么就会出错!解决办法,父类在重载构造器后,必须再显示定义无参构造器;或者在子类中构造器中使用super调用父类中可以调用的构造器。

8.2 继承性的说明&特性

  • 1、子类会继承父类所有的实例变量和实例方法,因此继承意味着子类的对象除了看子类的类模板还要看父类的类模板。

    • 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
    • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
      子类继承父类的属性/方法
  • 2、封装性对继承性的影响:子类不能直接访问父类中私有的(private)的成员变量和方法:子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。
    理解:能继承不代表对象能访问,就父类本身而言,如果属性私有化,他都未必能访问,不能访问不代表他不拥有!!!继承与封装是不冲突的

  • 3、继承的关键字“extends”:子类不是父类的子集,而是对父类的“扩展”:子类在继承父类以后,可以对父类功能上的扩展(体现:增加特有的属性、方法)

  • 4、Java支持多层继承(继承体系):即直接父类、间接父类。子类、父类是一个相对的概念;顶层父类是Object类。如果没有指定继承类,那么该类默认继承java.lang.0bject。

  • ①父类:子类=1:多;单继承:一个子类只能继承一个父类。

总结:这些特性可以按人来类比,比如一个人有爸爸、爷爷,爸爸可以有多个孩子,孩子继承爸爸,并且可以有一些扩展,但是如果有些东西被锁起来了,你虽然拥有但是不能调用,可以通过其他的有权限的来间接访问。

8.3 方法的重写override/overwrite

为什么需要方法重写?
子类继承父类声明的所有实例变量和实例方法,但是父类方法不太适用于子类,即子类需要对从父类中继承来的方法进行改造。
1、方法的重写定义:也称为重置覆盖。子类对父类继承过来的方法进行覆盖、复写的操作。
在程序执行时,子类的方法将覆盖父类的方法。

2、子类重写的方法的格式:

  • 与方法的声明格式一致:[修饰符] 返回值类型 方法名(参数列表){方法体}
  • 子类重写的方法加上注解@Override:用来检测是不是满足重写方法的要求(比如检查格式),也可以起一个标注重写的方法的作用。如果省略,只要满足要求,也是正确的方法覆盖重写。

3、方法重写的要求:子类重写的方法VS父类被重写的方法。在开发时一般是定义为一样的就是方法体不一样。

  • 他们的方法名称参数列表必须要相同
  • 返回值类型:①引用数据类型:子不能大于父(例如:Student < Person);②基本数据类型必须要相同;③void必须要相同。
  • 抛出的异常:子不能大于
  • 访问权限:子不能小于父(public > protected > 缺省 > private)。因为如果子类的权限小于父,也覆盖不住父。
    注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法(缺省的权限只在本包)也不能重写——→总结起来就是子类没有权限的方法不能重写!

注意点:子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

4、方法重写VS方法重载overload
方法的重载:“两同一不同”。方法名相同,形参列表不同。不看返回值类型。
方法的重写:起覆盖作用。方法名和参数列表必须相同,返回值类型、异常抛出、访问权限也有大小要求,为了能够覆盖住父类中被重写的方法。【一般会定义成只有方法体不一样】

问题:如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗?
不是,这就是子类的一个新的方法了。从父类继承过来的方法虽有拥有,但是没有访问权限,自己新定义的这个就不一样了。这与在同一个类中的不能定义一个同方法名同参数列表的规则不一样。
因为子类会继承父类中所有的方法,继承的方法也在子类中,如果又在子类中定义了一个同名但是参数列表不同的方法就是重载方法。
但是如果与继承的方法同名,且参数列表又是一样的,就是重写,但是同时还需要注意一些其他要求(权限修饰符、抛出异常、返回值类型)

重写VS重载

  • 在同一个类中
  • 在父子类中:也可以定义重载,因为子类继承父类的方法,这个方法也相当于就在子类中了。
//同一个类中
public class TestOverload {
    public int max(int a, int b){
        return a > b ? a : b;
    }
    public double max(double a, double b){
        return a > b ? a : b;
    }
    public int max(int a, int b,int c){
        return max(max(a,b),c);
    }
}
//父子类中
public class TestOverloadOverride {
    public static void main(String[] args) {
        Son s = new Son();
        s.method(1);//只有一个形式的method方法

        Daughter d = new Daughter();
        d.method(1);
        d.method(1,2);//有两个形式的method方法
    }
}

class Father{
    public void method(int i){
        System.out.println("Father.method");
    }
    private void method2(int i){
        System.out.println("Father.method");
    }
}
class Son extends Father{
    public void method(int i){//重写
        System.out.println("Son.method");
    }
}
class Daughter extends Father{
    public void method(int i,int j){//重载
        System.out.println("Daughter.method");
    }
    
    public void method2(int i,int j){//这是一个新的方法,已经和原来父类中私有化的方法不一样了。
        System.out.println("Daughter.method");
    }
}

8.3 Object类(超类)

8.3.1 Object类的介绍&理解

1、Object类说明:类 java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。因此Object类中声明的结构(属性、方法)就具有通用性

  • Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用。→如果一个类的对象使用Object来接收,那么多态性的弊端也是避免不了的。
  • 所有对象(包括数组)都实现这个类的方法。→java中的容器会存在一些通用的方法,本质上都是因为他们继承了Object超类。因此以后在使用引用类型的对象时可以调用Object超类中的方法实现一些功能。
  • 如果一个类没有特别指定父类,那么默认则继承自Object类。

2、一些理解

  • 如果java中定义的方法形参类型是Object obj,由于所有其它类都是继承Objectl类的,因此其他类类型都可以作为形参传入
  • 如果使用Object类型来接收其他类的对象,那么只能调用子类中重写Object类的方法(如8.3.2中介绍的),或者Object类中定义的一些方法,子类中特有的只有在强转后才能调用→这也给了我们一些启发,重写Object类中的方法,后面在调用的时候就是我们在子类中重写的方法,而不是java的Object类中默认定义的。
    举例:equals(Object obj):因为定义的形参类型是Object,其他所有的类都是它的子类,因此可以传入不同类的类型作为形参。

重写java中默认定义的方法举例
企业面试真题

//法一:退出
public static void method(int a, int b) {
	// 在不改变原本题目的前提下,如何写这个函数才能在main函数中输出a=100,b=200?
	a = a * 10;
	b = b * 20;
	System.out.println(a);
	System.out.println(b);
	System.exit(0);
}
//法二:
public static void method(int a, int b) {
	PrintStream ps = new PrintStream(System.out) {
		//重写父类的方法
		@Override
		public void println(String x) {
			if ("a=10".equals(x)) {
				x = "a=100";
			} else if ("b=10".equals(x)) {
				x = "b=200";
			}
			super.println(x);
		}
	};
	System.setOut(ps);
}
8.3.2 Object中方法(12/6:equals/toString/clone/finalize/getClass/hashCode())

Object类中没有声明属性,提供了一个无参的构造器。
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。这里我们主要关注其中的6个:
Object中声明的方法
使用场景
getClass()反射、hashCode()集合、notify()/notifyAll()/wait()/wait(xx)多线程
1、equals()方法:格式:obj1.equals(obj2),任何引用类型。可以使用IDEA自动生成重写方法来比较equals()方法
= =

  • 基本类型:只要两个变量的值相等,即为true。
  • 引用类型:比较地址值!指向同一个对象时为true。
  • 用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错

equals():自定义的类在没有重写Object中equals()方法的情况下,调用的就是默认的,比较两个对象的地址值是否相同(比较是否指向堆中的同一个对象实体)。只能比较引用类型,Object类源码中equals()的作用与“==”相同。

  • 注意:对于类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不是地址值;因为java在这些类中重写了Object类的equals()方法。
  • 当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等。重写equals()方法的原则。
    • 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
    • 自反性:x.equals(x)必须返回是“true”。
    • 传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
    • 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
      任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。

总结:不重写的equals的功能和"=="是一样的,在开发中经常需要比较实体内容是否相等,因此平时在使用equals时,一般情况下也是重写的,用来比实体内容,而不是地址值。→手动、IDEA自动生成。

重写equals()方法,由于经常会用到重写来之比较实体内容,因为IDEA也提供了自动生成

//Object类中关于equals()方法的定义:比较两个引用类型对象的地址值是否相等
public boolean equals(Object obj){
	return (this == obj);//判断这两个对象的地址是否相等
}

//法一:手动重写equals():比较两个引用类型的实体内容
//法二;IDEA自动生成
class User{
	String name;//String中已经重写了Objectd中的equals(),比的是内容,对于String类型,如果使用"=="比的是地址。
	int age;
	public boolean equals(Object obj){
		if(this == obj){
			return (this == obj);//快速判断,因为如果地址相等了,内容自然也是相等的。
		}
		if(obj instanceof User){//判断obj是否是一个引用类型
			if(obj == null || getClass() != obj.getClass()) return false;
			User user = (User)obj;
			/**
			if(this.age == user.age && this.name.equals(user.name)){//String类型使用equals比的是内容
				return true;
			}else{
				return false;
			}
			*/
			return this.age == user.age && this.name.equals(user.name);
		}
	}
}

//后面再创建User对象,调用equals()方法比的就是实体内容。

2、toString()方法
方法签名:public String toString()

  • 默认情况下,通过源码关于toString()方法的定义可以知道,它的返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
  • 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()。【java源码中,会对不同的数据类型重写toString()方法,导致了对于不同的类型是按照不同规则打印的】
    因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示 出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
  • 像String、File、Data或包装类等Object子类:都重写了toString()方法,在调用toString()方法时,返回当前对象的实体内容。
  • 在自定义的类中,在没有重写Object类的toString()情况下,默认的返回值就是当前对象的地址值;如果重写了调用的就是重写toString()方法。

开发启示:开发中对于自定义的类中在调用toString()时,也希望显示其对象的实体内容,而非地址值。此时,就需要重写Object类中的toString()方法。IDEA可以自动生成。

源码中的toString()定义、自定义的

//源码中对于对象的实现打印
public String toString(){
	return getClass().getNmae() + "@" + Integer.toHexString(HashCode());//转换成十六进制
}
//可以在子类中重写toString方法
public String toString(){
	return super.toString();//调用父类的方法,还是返回的父类的那种
	//return "1";
}
//2可以在子类中重写toString方法
public String toString(){
	return a+b;//自定义返回值的形式
}

3、clone()方法:格式:对象名.clone()→这也是创建对象的一种方式
克隆得到的对象与原对象不是同一个,在堆内存中的不同地址。
Object类中定义:protected Object clone(),由于权限只能是Object不同包下的子类,一般情况下需要重写这个方法,从而可以在本包下不是该类的子类中调用该方法。

4、finalize()方法(已过时)

  • 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
    • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
  • 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
  • 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
    • 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
  • 在JDK 9中此方法已经被标记为过时的。→会导致的问题:导致内部出现循环引用,导致此对象不能被回收。

在子类中重新给equals()、toString()、clone()、finalize()方法示例

  • 重写Object中toString方法,与clone()方法不同的是,这个方法就算在子类中不重写,在该子类的非子类中调用也是可以的。
  • 在子类中重写clone的格式,以便该类的同包下的非子类调用该方法
//Object类的clone()的使用
public class CloneTest {
	public static void main(String[] args) {//或者在这里抛出异常:throws CloneNotSupportedException
		Animal a1 = new Animal("花花");
		try {
			Animal a2 = (Animal) a1.clone();
			System.out.println("原始对象:" + a1);
			a2.setName("毛毛");
			System.out.println("clone之后的对象:" + a2);
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		a1.toString();//调用子类重写的toString()方法。
		al = null;;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
		System.gc();//强制性释放空间
		//因为这里调用System.gc();后程序就结束了,对象肯定也被回收了,所以为了验证,需要使用sleep()是程序等待一下。
		
	}
}
class Animal implements Cloneable{
	private String name;
	public Animal() {
		super();
	}
	public Animal(String name) {
		super();
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	//重写equals()方法
	public boolean equals(Object obj){
		//一些代码逻辑实现
		super.equals();
	}
	//重写toString()方法
	@Override
	public String toString() {
		return "Animal [name=" + name + "]";
	}
	//重写Object中clone()方法,权限修饰符可以是protected、public
	@Override
	protected Object clone() throws CloneNotSupportedException {
		// TODO Auto-generated method stub
		return super.clone();
	}
	//子类重写finalize()方法,可在释放对象前进行某些操作
	@Override
	protected void finalize() throws Throwable {
		System.out.println("对象被释放--->" + this);
	}
}

5、getClass()方法

6、hashCode()方法

九、面向对象特征3—多态(较难)

继承性的一个延续,没有继承性也就没有多态性!有继承性才会有多态性!
理解:一个事务的多种形态。比如声明"宠物"但是他具体的形态可以是多种;声明"玩具"它的形态可以是多种;"技术科的同事"有很多个人。
本质就是可以使用父类类型接收不同的子类类型对象,【向上转型】

9.1 多态的介绍和理解

编译和运行时类型不一致
广义:子类对象的多态性、方法的重写;方法的重载(重载也不能算)
1、多态性的理解(狭义):父类的引用指向子类的对象。(或子类的对象赋给父类的引用,可以说是子类对象的多态性)。
→多态性的使用前提:要有类的继承关系、要有方法的重写→多态的本质还是比较适合父类是接口,子类是实现类的情形;或者作为形参使用。
格式:父类类型 变量名 = 子类对象;【父类类型:指子类继承的父类类型,或者实现的接口类型】
2、多态的应用:虚方法调用:编译看左边,执行看右边。
在子类中重写了父类的方法的情况下,使用父类调用它的方法时,编译时,认为方法是左边声明的父类类型的方法(即被重写的方法),执行时,实际执行的是子类重写父类的方法。
虚方法调用理解:屏蔽了子类Man类特有的属性和方法。

3、多态性的适用性:适用于方法,不适用于属性。属性不满足多态性!
4、多态的理解

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法);“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)。
Person e = new Student();
e.getInfo();	//如果Student类中对于该类进行了重写,那么调用就是Student类的getInfo()方法
e.num;//子父类中都存在属性num,但是由于属性不存在多态性,因此这里调用的是父类中的属性。

4、多态的好处&弊端

  • 好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
    举例:定义一个方法,它的形参只需要定义为父类类型的,那么这个形参可以传入该父类,也可传入不同的子类。【向上转型】
  • 弊端:创建了子类对象,也加载了子类特有的属性和方法,但是由于声明为父类的引用导致不能直接调用子类特有的属性和方法。(不能直接调用,但是可以通过向下转型为子类类型来调用子类中特有的属性和方法)
    理解:父类调不了子类的东西,当然重写的方法除外!

5、多态的主要使用场景

  • 方法内局部变量的赋值体现多态:局部变量的类型统一使用父类类型,可以接受不同的子类类型创建的对象。
  • 方法的形参声明体现多态:方法的形参为父类类型,那么也可以传入不同的子类类型。
  • 方法返回值类型体现多态:方法返回值可以是不同的子类类型,但是接收可以统一使用父类类型接收。

为什么需要多态性总结:设计一个数组、成员变量、方法形参、返回值类型时,无法确定他的具体类型,只能确定他是某个系列的类型。

多态性决解的问题举例
下面的adopt、feed方法的参数可以同设置为animal类类型,可以传入其不同的子类类型dog、cat

public class Cat {
    public void eat(){
        System.out.println("猫吃鱼仔");
    }
}
public class Dog {
    public void eat(){
        System.out.println("狗啃骨头");
    }
}
public class Person {
    private Dog dog;
    //adopt:领养
    public void adopt(Dog dog){
        this.dog = dog;
    }
    //feed:喂食
    public void feed(){
        if(dog != null){
            dog.eat();
        }
    }
    /*
    问题:
    1、从养狗切换到养猫怎么办?   
    	修改代码把Dog修改为养猫?
    2、或者有的人养狗,有的人养猫怎么办?  
    3、要是还有更多其他宠物类型怎么办?
    如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
    */
}

6、扩展

  • 静态链接(早期绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。
  • 动态链接(晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

9.2 向上转型(多态本质)和向下转型(多态逆过程)

1、引用类型再分类:

  • 编译时类型:由声明该变量时使用的类型决定
  • 运行时类型:由实际赋给该变量的对象决定

“编译时看左边,运行时看右边”:一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

2、向上转型/向下转型理解

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
      向上转型和向下转型理解

3、为什么需要类型转换——多态的弊端
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过
4、如何实现向上/向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量

9.2 instanceof关键字

解决的问题:在向下转型时也不是不管什么类型之间都能随便转换的,(包括java中的基本数据类型强转都是有一定规则的)可能会出现ClassCastException的问题(编译不会报错,运行会出错)
1、instanceof作用:给引用变量做类型的校验。即判断对象与类之间的关系。
举例:a instanceof A,判断对象a是否是类A的实例,如果对象a所属的类是类A的子类B,那么这个语句返回值也是true。
使用要求:要求对象a所属的类与类A必须是子类和父类的关系,否则会出现编译错误。
2、使用建议
在向下转型之前,使用instanceof判断,避免出现ClassCastException类型转换异常

十、关键字的使用

关键字的作用:实现面向对象的3个特征,修饰类即类的成员!

10.1 package\import

一个用来指明,一个用来导入调用!
1、package(包):用于指明文件中定义的类、接口等结构所在的包,便于调用。类比于类是哪个班级的。

  • 语法格式:package 顶层包名.子包名 ;
  • 包的特点:
    • package语句:一个源文件只能有一个;且是第一条语句,如果缺省表示"无名包"
    • 包名:属于标识符,满足标识符命名的规则和规范(全部小写)。比如公司域名的倒置,不要java.xxx,会报错SecurityException,这是禁止的!因为加载类使用类加载器,会分析类是属于哪个包的,看到是java开头的就会认为是JDK提供的API。
    • 包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示一层文件目录。
    • 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)
  • 包的作用:
    • 包可以包含类和子包,划分项目层次,便于管理
    • 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式
    • 解决类命名冲突的问题
    • 控制访问权限
  • 包的实际应用:比如MVC设计模式,放在不同的包下,后期也可以通过完整的包名进行调用。
  • JDK中主要的包介绍
    • java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能
    • java.net----包含执行与网络相关的操作的类和接口。
    • java.io ----包含能提供多种输入/输出功能的
    • java.util----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
    • java.text----包含了一些java格式化相关的类
    • java.sql----包含了java进行JDBC数据库编程的相关类/接口
    • java.awt----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。【基本淘汰了,不用看!】

2、import关键字:为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。相当于import语句告诉编译器到哪里去寻找这个类。

  • 语法格式:import 包名.类名;
  • 注意事项:
    • import语句:在包的声明package和类的声明之间;多个import语句导入多个类/接口。如果需要这个包的子包下的类仍然需要导入
    • import a. * :表示可以导入a包下的所有的结构。举例:可以使用java.util.*的方式,一次性导入util包下所有的类或接口。
    • java.lang包下/当前包下:导入类或接口,可以省略此import语句。
    • 导入不同包下的同名的类:需要使用类的全类名的方式指明调用的是哪个类,此时不要用import。
    • import static 组合的使用:调用指定类或接口下的静态的属性或方法(这都是属于类的,直接用类调用,java的工具方法基本都是用static定义的,import类就可以调用相关的方法了。不是对象的)。比如import static java.lang.System.out,调用out.println()方法。

10.2 this关键字(2个位置3个调用结构)

回顾Java类的成员:成员变量(实例变量)、成员方法(实例方法)、构造器、代码块、内部类
1、this可以使用的位置:本类中的实例变量、实例方法——分别可以表示的含义:调用该方法的对象(当前对象)、该构造器正在初始化的对象(当前正在创建的对象)
2、this可以调用的结构:本类中的实例变量、实例方法、构造器——这也很好理解,因为this在不同位置所表示的含义"对象",那么对象在构造器中三种结构肯定是都可以调用的,然而当对象在方法中只能调用结构实例变量、实例方法,构造器是不能调用的!
3、this使用在不同的位置及其表示的含义、可以调用的结构

  • 实例方法内(注意是非static):this表调用该方法的对象。
    • 可以使用this调用当前类的成员变量、实例方法,表示当前属性/方法所属的对象。以便增强程序的可读性(一般情况下省略this)。
  • 构造器内:this表示表该构造器正在初始化的对象。
    • 可以使用this调用成员变量、实例方法、构造器(同一个类中构造器相互调用)——语法this(形参列表)
    • 调用构造器的注意点:
      • ①不能出现递归调用(调用构造器自身)→n个构造器,最多有n-1个构造器中使用this调用其他构造器;
      • ②只能声明在构造器首行→在一个构造器中最多只能声明一个this(即调用一个构造器);
      • ③构造器中调用其他构造器,在创建对象的时候只创建了一个对象,因为构造器只有搭配new才能创建对象,在构造器中调用的其他构造器并没有搭配new,因此就算调用了也不会创建对象的!

特殊情况this不能省:在实例方法内或构造器内,当其形参与成员变量同名时,成员变量必须添加this标明。即使用this来区分成员变量局部变量。另外如果使用this访问属性时,如果本类中没找到,会再去父类中查找。
总结:this无论是使用在实例方法还是构造器内,这两种都是需要调用/初始化才会生效,因此只要在生效的时候this才有了其具体的含义。

4、this实际意义&应用
需求场景1:实例方法或者构造器中,this调用当前对象的成员,成员变量与形参同名时不能省略!
比如:在类中声明一个属性对应的setXxx()方法时,如果方法的形参名和属性名同名了,那么该如何区分这两个变量呢?
说明:使用this调用的是属性,没有的是形参。
需求场景2:同一个类中构造器相互调用,即this在构造器中使用,调用构造器。
语法格式:①this():调用本类的无参构造器;②this(实参列表):调用本类的有参构造器
作用:构造器中需要定义很多的逻辑代码,那么多个重载的构造器中都需要写那么多代码太麻烦!!!因此可以使用this在不同的构造器中相互调用。但是如果所有构造器中的代码逻辑都是一致的话可以在类中定义一个私有化方法在多个构造器中调用。

总结this突出的作用:区分形参和成员变量;构造器之间相互调用,提高代码的重用性;解决不同构造器间的代码重复问题!

class Person{/ 定义Personprivate String name ;	
	private int age ;	
	//无参构造器
	Person(){
	//假设有50行代码逻辑
		this("",18)//同时也引用了这个构造器中的20行代码逻辑
	}
	public Student(String name) {
        //this();//调用本类其他无参构造器,三个构造器调用必须要有一个没有调用构造器,否则就会出现循环调用
        this.name = name;
    }
	//位置1:this在构造器内使用
	public Person(String name,int age){	
		//假设有20行代码逻辑
		this();//this调用其他无参构造器,同时也引用了这个构造器中的50行代码逻辑
		this.name = name ;//this调用属性,即成员变量this.name,而name表示形参
		this.age = age ; 
		this.speak();//this调用方法
    }
    //位置2:this在方法(非静态)内使用
    public void setName(String name){//this只有在构造器中才能调用构造器
        this.name = name;//this调用属性
        this.speak();//this调用方法
    }
    public void setAge(int age){
        this.age = age;
    }
	public void getInfo(){	
		System.out.println("姓名:" + name) ;//方法没有形参,属性直接调即可,无需使用this修饰区分
		this.speak();//this调用方法
	}
	public void speak(){
		System.out.println(“年龄:” + this.age);	//this调用属性
	}
}
//———————————————分割—————————————————————————————————
//this定义的不同位置,表示的不同含义
Person person = new Person(1,"123");//调用了构造器,那么构造器内定义的this表示正在初始化的对象
person.setNum(2);//调用了方法,那么方法内定义的this表示调用该方法的对象。

10.3 super关键字

10.3.1 super介绍

super、this这两个关键字记住一个原则:一个直奔父类,一个是本类。
基于继承性才出现的
1、使用的位置:子类中的构造器、代码块、成员方法内——表示在子类中调用父类
2、可以调用的结构:父类的属性、成员方法、构造器

  • super调用父类的属性、方法:需要满足封装性的前提下,调用格式"super.",一般情况下可以省略,但是如果子类重写了父类的方法或者子类属性和父类属性同名的情况必须要用super。【开发要求,方法重写无法避免,但是实际开发中最好避免子父类中定义同名的属性】
    • 说明:在子类中查找调用的方法、属性遵循的原则都是"就近原则":局部变量→本类中→父类(直接父类→间接父类),如果加上this起点是本类中的成员,如果加上super起点是父类中的成员。
  • super调用父类的构造器(注意只能在子类的构造器中调用)
    • ① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。
    • ② 规定:“super(形参列表)”,必须声明在构造器的首行。
    • ③在构造器的首行,“this(形参列表)” 和 "super(形参列表)"只能二选一。
    • ④如果在子类构造器的首行既没有显示调用"this(形参列表)"、"super(形参列表)",则子类此构造器默认调用"super()",即调用父类中空参的构造器。——→可以得到开发中常见的错误:如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错
      理解:子类的构造器要么调用本类中其他构造器要么调用父类的构造器,如果什么都不声明就默认调用父类的无参构造器【开发中如果重载构造器,最好要加上默认的无参构造器】
    • ⑤由③④可以得出结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。注意:就算使用this调用本类的构造器,最终还是会间接的调用父类的构造器!!!
    • ⑥由⑤可以得到一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)“,则剩下的那个一定使用"super(形参列表)”。

==我们在通过子类的构造器创建对象时,一定会在调用子类构造器的过程中,直接或间接的调用到父类的构造器,也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法。==而其父类同样会调用到其父类的构造器。。。直到调用了object类的构造器位为止。
注意:子类的代码块、成员方法内不能调用父类的构造器。

为什么需要super?
重写方法后需调用父类中的:子类继承父类后,对父类的方法进行了重写,那么在子类还需要对父类被重写的方法进行调用时应该使用super表示父类。
父子类定义了同名的属性:子类继承父类后,发现子类和父类定义了同名的属性,使用super区分两个同名的属性。属性是不存在覆盖的说法,因为他们存储的对内存位置都不一样。【实际开发中子父类不建议定义同名的属性】
在创建子类对象的过程中,一定会调用父类中的构造器吗? yes!
创建子类的对象时,内存中到底有几个对象?只有一个,因为构造器搭配new只有一个对象。构造器的作用只是初始化。

调用父类方法、属性测试

//父类
class Father{
    int a = 10;
    int b = 11;
    public void call(){
        System.out.println("打电话");
    }
    public void showNum(){
        System.out.println("来电显示号码");
    }
}
//子类
class Daughter extends Father{
    //子类中定义的属性与父类同名
    int a = 20;
    //重写父类的来电显示功能的方法
    public void showNum(){
        //来电显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");
        //保留父类来电显示号码的功能
        super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
    }
    public void test(){
    	//调用方法
    	showNum();//自己这个类中的
    	this.showNum();//自己这个类中的
    	super.showNum();//父类中的
        //子类与父类的属性同名,子类对象中就有两个a
        System.out.println("子类的a:" + a);//20  先找局部变量找,没有再从本类成员变量找
        System.out.println("子类的a:" + this.a);//20   先从本类成员变量找
        System.out.println("父类的a:" + super.a);//10    直接从父类成员变量找

        //子类与父类的属性不同名,是同一个b
        System.out.println("b = " + b);//11  先找局部变量找,没有再从本类成员变量找,没有再从父类找
        System.out.println("b = " + this.b);//11   先从本类成员变量找,没有再从父类找
        System.out.println("b = " + super.b);//11  直接从父类局部变量找
    }
}
10.3.2 this与super对比

1、this和super的意义

  • this:当前对象
    • 在构造器和非静态代码块中,表示正在new的对象。
    • 在实例方法中,表示调用当前方法的对象
  • super:引用父类声明的成员

2、this和super的使用格式

  • this
    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
    • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super
    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

总结:

  • this表示本类,适用于形参(即局部变量)与本类的属性同名时,super表示父类,适用于本类方法重写父类方法时(这肯定会导致子父类方法重名的)以及子父类的属性名同名时。
  • 接下来就是在构造器中使用相互之间调用构造器,记住一个准则:构造器中要么调用本类中重载的构造器要么直接/间接的调用父类的构造器,如果啥也没有就默认调用父类的无参构造器,注意:就算使用this调用本类的构造器,最终还是会间接的调用父类的构造器!!!即不管存不存在this、super,最终都会调用父类的构造器
10.3.3 子类对象实例化全过程

结合子父类一起实例化的过程:super、this

实例化分析:代码举例

  • 从结果来看:创建子类对象后,子类就继承了父类中生命的所有属性和方法,在权限允许的情况下,可以直接调用
  • 从过程的角度来看:在通过子类的构造器创建对象时,一定会在调用子类构造器的过程中,直接或间接的调用到父类的构造器,也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法。而其父类同样会调用到其父类的构造器。。。直到调用了object类的构造器位为止。
class Creature {
    public Creature() {
        System.out.println("Creature无参数的构造器");
	}
}
class Animal extends Creature {
    public Animal(String name) {
        System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
    }
    public Animal(String name, int age) {
        this(name);
        System.out.println("Animal带两个参数的构造器,其age为" + age);
	}
}
public class Dog extends Animal {
    public Dog() {
        super("汪汪队阿奇", 3);
        System.out.println("Dog无参数的构造器");
    }
    public Dog(int n) {
        //这里虽然没有this、super,但是会默认调用父类的无参构造函数
        System.out.println("Dog无参数的构造器");
    }
    public Dog(int n,int m) {
        //这里虽然调用的是本类的构造器,但是在这个构造器中有默认调了父类的无参构造函数
        this(3);
        System.out.println("Dog无参数的构造器");
    }
    public static void main(String[] args) {
        new Dog();
	}
}

下图中,方块表示构造器的调用:
子父类调用构造器实例化的全过程

10.3 static关键字(修饰类成员除构造器)

静态结构属于类,随着类的加载,在静态结构中不能使用非静态结构(因为创建的晚),反之可以!
为什么需要static?
如果想要一个成员变量被所有实例共享,那么就使用static修饰,称为类变量(或类属性)
成员变量:实例变量/属性(属于对象)、类变量/类属性(属于类的)
1、变量的分类

  • 数据类型不同:基本数据类型(8种)、引用数据类型(数组、类、接口、枚举、注解、记录)
  • 变量在类中声明的位置不同:
    • 成员变量:实例变量(属性,属于对象的,非静态)、类变量(类属性,属于类,静态static)
    • 局部变量(方法/构造器/内部类/代码块等内或其形参):也分为静态和非静态的。
  • static修饰:静态变量、非静态变量

2、静态和非静态的区别
静态变量/静态方法:也称为类变量/类方法,通过类调用,所有对象共享这一份,如果权限修饰符允许也可以通过对象访问,但是不影响只有一份的概念
非静态变量/非静态方法:也称为实例变量(属性)/实例方法,只有创建对象后才能调用,每个对象独自拥有一份

3、static可以修饰的:属性、方法、代码块、内部类,除了狗构造器
4、被static修饰后的成员特点:随着类的家加载而加载;优先于对象存在;被所有对象所共享;访问权限允许时可以不创建对象直接被类调用。
因为静态的优于非静态(创建对象后才存在)存在,因此在类调不了非静态方法和属性,静态方法中不能调用非静态结构,反过来非静态结构可以调用静态结构
接下来详细介绍被static修饰的几个成员

10.3.1 成员变量(静态/实例)

1、语法格式

[修饰符] class{
	[其他修饰符] static 数据类型 类名;
}

2、实例变量VS静态变量

  • 个数
    • 静态变量:在内存空间只有一份,被多个对象所共享
    • 实例变量:类的每一个实例(对象)都保存着一份实例变量
  • 内存位置
    • 静态变量:【方法区名字JDK6/7中叫做永久代代;JDK8叫元空间】JDK6放在方法区,JDK7/8放在堆空间(专门一个静态域存放)
    • 实例变量:存放在堆空间的实体中
  • 加载时机
    • 静态变量:随着类的加载而加载,由于类只会加载一次,静态变量只会有一份
    • 实例变量:随着对象的创建而加载,每个对象拥有一份实例变量
  • 调用者
    • 静态变量:类、对象都可以调用
    • 实例变量:只有对象能调用
  • 消亡时机
    • 静态变量:随着类的卸载而消亡
    • 实例变量:随着对象的消亡而消亡
10.3.1 方法(静态/实例)

1、静态方法的语法格式

[修饰符] class{
	[其他修饰符] static 返回值类型 方法名(形参列表){
        方法体
    }
}

2、静态方法特点
随着类的加载而加载
可以通过类调用,也可以通过对象调用,而实例方法只能通过对象调用。注意静态方法中可以调用静态结构,但是不可以调用非静态属性/方法(因为静态先于非静态加载而存在)。而在类的非静态方法中可以调用当前类的非静态结构或静态结构。
静态方法不能使用this和super(这两个代表的是对象)

3、思考:什么时候需要将属性声明为静态的?
判断当前类的多个实例(对象)是否被类的多个对象所共享,且此成员变量的值是相同的
常一些常量声明为静态的。比如Mh中的PI

什么时候将方法声明为静态的?
方法内操作的变量如果都是静态变量(而非实例变量)的话
常将工具类中的方法生命是静态的,比如Math类的的API

比如:静态变量对应的get\set方法,为了他们的生命周期能够一样

思考:诸如数组这类的,Arrays可以调方法,使用自己定义的数组类型也可以调用;又比如Math等等。因为他们是静态方法,可以用类或者对象调用都行。

10.3.3 static的一个应用(单例模式)

1、设计模式:在大量的实践中总结理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。“套路”
2、经典的设计模式:特定环境下特定问题的处理方法
经典的设计模式
3、单例模式:采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象的实例方法。
设计思路:类在虚拟机中只能产生一个对象,①类的构造器私有化,外部不行,本类可以使用new创建;②该类提供一个方法返回类内部创建的对象,只能是静态的方法(因为非静态方法只能通过对象调用,否则new不了对象就调不了方法),外在外部通过类去调用方法;③静态方法中只能调访问静态成员变量,这个对象实例也只能是static的(全局只能有这一个对象)
即将构造器私有化,禁止外部创建对象,外部想要创建对象只能通过提供的一个全局访问点才能创建。因为全局只有一个对象,因此对象也是静态的。

法一:懒汉式:

  • 优点:延迟加载,,节省内存空间
  • 缺点:线程不安全
//懒汉式:调用创建对象方法时才创建
public class Singleton{
	private Singleton(){}//私有化无参构造器:避免外部创建
	// 2.内部提供一个当前类的实例
    // 4.此实例也必须静态化
	private static Singleton singleton;//全局只有这一个对象,因此也设置为静态的
	 // 3.提供公共的静态的方法,返回当前类的对象
	public static Singleton getInstance(){
		if(singleton == null){//获取全局变量对象判断其是否为null
			return new Singleton();
		}
		return singleton;
	}
}

饿汉式:

  • 优点:立即加载,由于在内存中较早加载,使用方便、更快。线程安全
  • 缺点:在内存中占用时间较长,容易造成内存泄漏(GC没有回收垃圾)
//饿汉式:不管你调没调用,都在一开始就给你创建好
public class Singleton{
	private Singleton(){}//私有化无参构造器
	private static Singleton singleton = new Singleton();//在一开始就先创建一个对象
	public static Singleton getInstance(){
		return singleton;
	}
}

4、单例模式使用场景
定义:单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

  • Windows的Task Manager (任务管理器)就是很典型的单例模式
  • Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  • Application 也是单例的典型应用
  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只 能有一个实例去操作,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
10.3.4 理解main方法

程序乳沟:JVM调用类的main()方法(public),且不需要创建对象,因此是static的,形参是String类型的数组参数,保存执行Java命令时传递给所运行的类的参数。
main()方法是静态的,在方法体中不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。
1、main()方法
理解1:看作普通的静态方法
理解2:程序入口,格式是固定的
2、与控制台交互
方式1:使用Scanner
方式2:使用main(String[] args)的形参传值:利用控制台、IDEA中设置(run→Edit Configuration→Program arguments)

public class CommandPara {
    public static void main(String[] args) {//程序入口
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}
//形参传值方式1:控制台
//运行程序CommandPara.java
java CommandPara "Tom" "Jerry" "Shkstart"

这是一个静态方法,在方法内,可以调用静态结构,但是不能调用非静态结构,只能通过创建类的对象后调用。

10.4 final关键字

10.4.1 final修饰(类/方法/变量)

1、final:最终的,不可更改的。【举例:太监】。
具体地:不能被继承、不能被重写、不能修改值(常量)
可以修饰:

  • 修饰类:这个类不能被继承,没有子类,但是可以被实例化。提高安全性,提高程序的可读性。表类无需再被扩展了,如:String类、System类、StringBuffer类
  • 修饰方法:子类继承父类后,不能重写该方法。功能已经确定了不需要再被修改,比如Object类中的getClass()
  • 修饰变量:赋值后就不能被修改,即常量,常量名建议用大写字母。
    • 成员变量:没有set方法,必须初始化,赋值方式:显式赋值、代码块中赋值,其中实例变量还可以在构造器中赋值,因为这些赋值方式保证了一个对象只有一个。
    • 局部变量:
      • 方体内声明的局部变量:再调用之前一定要赋值,一旦赋值就不可更改
      • 在调用此方法时给形参进行赋值,一旦赋值就不更改,不能在方法体中试图对其修改!注意:引用类型的形参属性可以变

final修饰成员变量

public final class Test {
	//显示赋值
    public static int totalNumber = 5;
    public final int ID;
	//代码块中赋值
	{
		ID = ++totalNumber;
	}
	// 可在构造器中给final修饰的“变量”赋值。注意类变量不行,只能是实例变量
    public Test() {
        ID = ++totalNumber; 
    }
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.ID);
    }
}

final修饰局部变量

//作为方法内部的参数
public class TestFinal {
    public static void main(String[] args){
        final int MIN_SCORE ;
        MIN_SCORE = 0;
        final int MAX_SCORE = 100;
        MAX_SCORE = 200; //非法
    }
}
//作为方法形参:如果是引用类型,对象不能变,但是它的内容可以变
public class Something {
    public int addOne(final int x) {
        return ++x;
        // return x + 1;
    }
}
//作为方法形参引用类型public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().addOne(o);
    }
    public void addOne(final Other o) {
        // o = new Other();
        o.i++;
    }
}
class Other {
    public int i;
}

10.4.2 final与static的搭配

通过final、static修饰的变量叫做全局常量

比如HashMap源码中定义的常量

static final int DEFAULT_INITIAL_CAPACITY =1 << 4;//默认初始化的容量
static final int MAXIMUM CAPACITY = 1 << 30;//默认最大的容量
static final float:DEFAULT_LTD_FACTOR = 0.75f;//默认加载因子

10.5 abstract关键字(抽象类/抽象方法)

10.5.1 抽象类/抽象方法介绍

父类没有办法给出具体的实现,交由子类各自具体实现。抽象方法必须要被重写。
可以修饰:类、方法
1、抽象类定义:使用abstract修饰的类叫做抽象类。必须要被继承,然后通过子类创建对象。自己不能创建对象。

  • 可以被继承,但是不能被实例化。理解:通过抽象对象调用抽象方法,而抽象方法没有方法体,这就没有意义。必须被继承然后在子类中实现(重写)抽象方法,然后通过子类对象调用实现的方法(虚方法的特性)。
  • 抽象类有构造器,因为子类对象实例化时会直接或间接调用父类构造器
  • 抽象类中可以没有抽象方法。反之,有抽象方法的类必定是抽象类,原因:抽象方法没有方法体,且是非静态的,但又希望该抽象方法不能被调用,因此定义在抽象类中,不能实例化对象,也就不能调用了。

2、抽象方法:使用abstract修饰,只有方法签名,没有方法体{}。必须在抽象类中,必须要被子类重写,不能被类/对象调用。

  • 抽象方法功能是确定的,只是不知道具体如何实现。
  • 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。原因:如果不重写抽象方法,那么子类继承父类也会继承该抽象方法。

实现Implenments:本质就是方法重写,只不过这里指的是子类对抽象父类中抽象方法的重写。

抽象类、抽象方法的语法格式

[权限修饰符] abstract class 类名{
	[其他权限修饰符] abstract 返回值类型 方法名([形参列表]);
}
[权限修饰符] abstract class 类名 extends 父类{
}

3、抽象类与final类对比
抽象类可以被继承,但是不能被实例化;final修饰的类不能被继承,但是可以被实例化。一个是非常确定不想再被扩展/重写,一个是不确定就想要被扩展/重写。
4、注意点
不能修饰成员变量、代码块、构造器,
abstract不能共用的关键字(自洽):

  • private私有方法:私有方法不能被重写
  • static方法:避免静态方法使用类调用。调方法的方式类/对象,static允许通过类调用,但是abstract两种都不允许,只能通过子类重写后创建对象调用
  • final的方法:不能被重写,abstract方法没有方法体,如果有不能被重写的话根本没有意义。
  • final的类:不能继承

为什么会用抽象方法?
在抽象类中的其他方法中可以用到功能,但是这个功能又不知道具体怎么实现,只有具体子类继承后才知道,那么可以定义抽象方法(可以理解为占个位置)
还有用在接口中,表示这个接口方法必须要被实现。

10.5.2 应用举例:模板方法设计模式(TemplateMethod)

1、模板模式设计:抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。【比如月饼的模具、四六级模板】
2、使用场景:有变有部变的

  • 当功能内部一部分实现是确定的,另一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
  • 即比如实现某个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

3、模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:

  • 数据库访问的封装
  • Junit单元测试
  • JavaWeb的Servlet中关于doGet/doPost方法调用
  • Hibernate中模板程序
  • Spring中JDBCTemlate、HibernateTemplate等

模板方法设计举例:

package com.atguigu.java;
//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {

	public static void main(String[] args) {
		BankTemplateMethod btm = new DrawMoney();
		btm.process();
		BankTemplateMethod btm2 = new ManageMoney();
		btm2.process();
	}
}
abstract class BankTemplateMethod {
	// 具体方法
	public void takeNumber() {
		System.out.println("取号排队");
	}
	public abstract void transact(); // 办理具体的业务 //钩子方法
	public void evaluate() {
		System.out.println("反馈评分");
	}
	// 模板方法,把基本操作组合到一起,子类一般不能重写
	public final void process() {
		this.takeNumber();
		this.transact();// 像个钩子,具体执行时,挂哪个子类,就执行哪个子类的实现代码
		this.evaluate();
	}
}
class DrawMoney extends BankTemplateMethod {
	public void transact() {
		System.out.println("我要取款!!!");
	}
}
class ManageMoney extends BankTemplateMethod {
	public void transact() {
		System.out.println("我要理财!我这里有2000万美元!!");
	}
}

4、思考
问题1:为什么抽象类不可以使用final关键字声明?
问题2:一个抽象类中可以定义构造器吗?
问题3:是否可以这样理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同?

十一、补充知识点

10.1 关于字节码

1、查看字节码:插件jclasslib:解析字节码文件,菜单栏"View"→"Show Bytecode With Jclasslib"
在out文件下可以查看到的是IDEA会反编译字节码文件(源文件编译后的反编译的字节码文件)
2、为什么需要字节码文件:编译器将源文件代码编译成字节码文件,才进行相关的解释。
比如:源文件中没有定义构造器,编译后会有默认的无参构造器;对类的属性(实例变量2.4中)赋值,生成的字节码文件中的指令都有体现他的赋值顺序;系统提供的默认的东西,编译后都会体现在字节码文件中。
利用jclasslib插件查看的字节码文件

构造器与其字节码文件方法分析(赋值)

1、字节码文件中的方法:构造器也称构造方法,但是要注意这个与普通的方法没关系!但是在生成字节码文件的时候,编译器会将构造器转换成一种特殊的方法,在字节码文件中可以看到归类为method。(即从字节码文件的角度:构造器会以<init>()方法的形态呈现,用以初始化对象)
2、方法特点:
方法都对应着一个类的构造器。(类中声明了几个构造器就会有几个)
编写的代码中的构造器在编译以后就会以方法的方式呈现
方法内部的代码包含了实例变量的显示赋值、代码块中的赋值和构造器中的代码。
方法用来初始化当前创建的对象的信息的。
构造器字节码文件

10.2 JavaBean

1、定义:JavaBean是一种Java语言写成的可重用组件。本质是一个类!
可以联想到Spring IOC中提到的Bean概念,它的设计就是符合这样的标准。
举例:扳手会在很多地方被拿去用。这个扳手也提供多种功能(你可以拿这个扳手扳、锤、撬等等),而这个扳手就是一个组件。
2、符合如下标准的Java类,叫做JavaBean,最基本的有下面三个

  • 类是公共的(public)
  • 有一个无参的公共的构造器(public):可以不用写,系统提供了默认的与类权限修饰符一致的无参构造器。
  • 有属性(private),且有对应的get、set方法(暴露出去,使得访问者可以对属性进行操作)

3、用途

  • 用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。
  • 《Think in Java》中提到,JavaBean最初是为Java GUI的可视化编程实现的。你拖动IDE构建工具创建一个GUI 组件(如多选框),其实是工具给你创建Java类,并提供将类的属性暴露出来给你修改调整,将事件监听器暴露出来。

10.3 UML类图

1、定义:UML(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言
UML工具软件不仅可以绘制软件开发中所需的各种图表,还可以生成对应的源代码。常见的工具有:PowerDesinger、Rose、Enterprise Architect。
在软件开发中UML类图的作用:可以更加直观地描述类内部结构(类的属性和操作)以及类之间的关系(如关联、依赖、聚合等)。

  • +表示 public 类型, - 表示 private 类型,#表示protected类型
  • 方法的写法: 方法的类型(+、-) 方法名(参数名: 参数类型):返回值类型
  • 斜体表示抽象方法或类
    UML类图

十二、企业真题

根据这些问题来理解知识点&掌握程度!

12.1 基础:类与对象&内存&封装性&构造器&属性赋值

1、面向对象,面向过程的理解?

  • 不管是面向过程、面向对象,都是程序设计的思路。
  • 面向过程:以函数为基本单位,适合解决简单问题。比如:开车
  • 面向对象:以类为基本单位,适合解决复杂问题。比如:造车

2、Java 的引用类型有哪几种
类、数组、接口;枚举、注解、记录
3、类和对象的区别

  • 类:抽象的,概念上的定义
  • 对象:具体的,类的一个一个的实例。

4、面向对象,你解释一下,项目中哪些地方用到面向对象?
“万事万物皆对象”。等后面用到再讲。

5、Java虚拟机中内存划分为哪些区域,详细介绍一下

  • Java中内存结构划分为:虚拟机栈、堆、方法区;程序计数器、本地方法栈
  • 虚拟机栈:以栈帧为基本单位,有入栈和出栈操作;每个栈帧入栈操作对应一个方法的执行;方法内的局部变量会存储在栈帧中。
    栈中的分配的几个部分是为了更好的优化,提出的想法,有的JVM提供了实现,有的没有。
  • 堆空间:new 出来的结构(数组、对象):① 数组,数组的元素在堆中 ② 对象的成员变量在堆中。
  • 方法区:加载的类的模板结构。

6、对象存在Java内存的哪块区域里面?
堆空间。
7、private 、缺省、protected、public的表格化作用区域
本类、本包、其他包的子类、其他包的非子类
8、main方法的public能不能换成private?为什么?
能。但是改以后就不能作为程序的入口了,就只是一个普通的方法。
9、造方法和普通方法的区别
编写代码的角度:没有共同点。声明格式、作用都不同。
字节码文件的角度:构造器会以<init>()方法的形态呈现,用以初始化对象。
10、构造器Constructor是否可被overload?
可以。
11、无参构造器和有参构造器的的作用和应用
用来初始化,new对象,并为属性赋值

12、成员变量与局部变量的区别
6个点。
声明的位置、内存中存放的位置、作用域、权限修饰符、初始化值、生命周期

13、变量赋值和构造方法加载的优先级问题
变量显式赋值先于构造器中的赋值。
如何证明?我看的字节码文件。

12.2 进阶:继承性&重写&super&多态&Object

1、父类哪些成员可以被继承,属性可以被继承吗?可以或者不可以,请举下例子。
父类的属性、方法可以被继承。构造器可以被子类调用。
2、什么是Override,与Overload的区别
重写:多态性的一种体现,子类中声明一个与父类方法名、参数列表相同的方法,实现子类的功能
重载:“两同一不同”:同一个类、方法名,参数列表不同。
3、verload的方法是否可以改变返回值的类型?
可以,返回值类型遵循一定的规则,①引用数据类型:子不能大于父(例如:Student < Person);②基本数据类型必须要相同;③void必须要相同。
public void method(int i){}
public int method(int j,int k){}
还有其他两个:

  • 抛出的异常:子不能大于
  • 访问权限:子不能小于父(public > protected > 缺省 > private)。因为如果子类的权限小于父,也覆盖不住父。

4、构造器Constructor是否可被override?
不能!构造器可以重载
5、为什么要有重载,我随便命名一个别的函数名不行吗?谈谈你是怎么理解的。
见名知意。
6、super和this的区别
把两个关键字各自的特点说清楚。
7、this、super关键字分别代表什么?以及他们各自的使用场景和作用。

8、谈谈你对多态的理解
类似问法:Java中实现多态的机制是什么&什么是多态?&Java中的多态是什么意思?
编译和运行时不一致

  • 广义上的理解:子类对象的多态性、方法的重写;方法的重载(重载也可以不算)
    狭义上的理解:子类对象的多态性。
  • 格式:Object obj = new String(“hello”); 父类的引用指向子类的对象。
  • 多态的好处:减少了大量的重载的方法的定义;开闭原则
    • 举例:public boolean equals(Object obj)
    • 多态,无处不在!讲了抽象类、接口以后,会有更好的理解。
  • 多态的使用:虚拟方法调用。“编译看左边,运行看右边”。属性,不存在多态性。
  • 多态的逆过程:向下转型,使用强转符()。
    • 为了避免出现强转时的ClassCastException,建议()之前使用instanceOf进行判断。

8、继承成员变量和继承方法的区别?
成员变量没有多态性,继承方法有多态性。
9、多态是编译时行为还是运行时行为?
只有运行的时候才知道真正加载进来的是哪个子类

10、多态new出来的对象跟不多态new出来的对象区别在哪?
Person p = new Man(); //虚方法调用。屏蔽了子类Man类特有的属性和方法。
Man m = new Man();
11、说你认为多态在代码中的体现
无处不在!后面学到了再说
比如:方法重写、方法中形参、构造器
12、与equals的区别(拓*思)
类似问法:两个对象A和B,A
B,A.equals(B)有什么区别(华油**普)

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
  • equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
  • 具体要看自定义类里有没有重写Object的equals方法来判断。
  • 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

13、重写equals方法要注意什么?

  • 明确判定两个对象实体equals()的标准。是否需要所有的属性参与。
  • 对象的属性,又是自定义的类型,此属性也需要重写equals()

14、Java中所有类的父类是什么?他都有什么方法?
相关问题:Object类有哪些方法?
toString()、equals()、clone()、finalize()、getClass()、hashCode()、wait()、sleep()、Notify()等等

12.3 高级:static&设计模式&main&代码块&final&抽象类和接口&内部类&接口

1、静态变量和实例变量的区别?说明静态变量和实例变量之间的区别和使用场景?
静态变量随着类的加载而加载,可以通过类/对象调用,不能被重写,不存在多态性。

2、是否可以从一个static方法内部发出对非static方法的调用?
不能,因为静态方法先加载,只能通过对象来对非静态方法的调用。

3、被static修饰的成员(类、方法、成员变量)能否再使用private进行修饰?
完全可以。除了代码块。

4、知道哪些设计模式?
单例模式、模板方法、享元设计模式(比如自动装箱,设置[-128,127范围的缓存对象)

5、开发中都用到了那些设计模式?用在什么场合?

6、main()方法的public能不能换成private,为什么?
可以改。但是改完以后就不是程序入口了。

7、main()方法中是否可以调用非静态方法?
只能通过对象来对非静态方法的调用。

8、类的组成和属性赋值执行顺序?&Java中类的变量初始化的顺序?
类的组成:成员变量、成员方法、构造器、代码块、内部类
属性赋值执行顺序:默认初始化、显式初始化/代码块初始化(静态优先于非静态)、构造器初始化、方法/对象属性赋值

9、静态代码块,普通代码块,构造方法,从类加载开始的执行顺序?
静态代码块 --> 普通代码块 --> 构造器

10、描述一下对final理解
修饰类不能被继承,修饰方法不能被重写,修饰变量表示常量不能改变

11、 判断题:使用final修饰一个变量时,是引用不能改变,引用指向的对象可以改变?
引用不能改变。
引用指向的对象实体中的属性,如果没有使用final修饰,则可以改变。

12、final不能用于修饰构造方法?
是的。

13、final或static final 修饰成员变量,能不能进行++操作?
不能。

14、 什么是抽象类?如何识别一个抽象类?
使用abstract修饰。

15、为什么不能用abstract修饰属性、私有方法、构造器、静态方法、final的方法?
为了语言的自洽。
16、内部类有哪几种?
成员内部类、局部内部类,有分为静态和非静态

17、内部类的特点说一下

18、匿名类说一下

19、谈谈你对面向对象的理解

  • 面向对象的两个要素:类、对象 —> 面向对象编程。“万事万物皆对象”。
  • 面向对象的三大特征
  • 接口,与类并列的结构,作为一个补充:类可以实现多个接口。

12.n 补充

1、常见的一些对比的问题:
方法重载、方法重写
throws /throw
final /finally / finalize
Collection /Collections
String /stringBuffer /stringBuilder
ArrayList /LinkedList
HashMap /LinkedHashMap /Hashtable

sleep()/wait()
== / equals()

;