面向对象
概述
万物皆对象。面向对象,是一种编程思想,是把一个整体的事物按照各个功能来进行划分。在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,描述成计算机事件的设计思想。 它区别于面向过程思想,强调的是通过调用对象的行为来实现功能,而不是自己一步一步的去操作实现。
面向过程
- 概述:需求(要做什么)——分析(具体怎么做)——实现(代码体现),整个过程中的每一个步骤都需要我们去操作和实现。
- 面向过程开发:就是面向着具体的每一个步骤和过程,把每一个步骤和过程完成后,由这些功能方法的相互调用,完成需求。
- 面向过程,强调的是每一个功能的步骤
- 面向过程的代表语言:C语言
面向对象
概述:面向对象是基于面向过程的编程思想。
面向对象,强调的是对象,然后由对象去调用功能,它只关注结果
面向对象开发:不断地创建对象,使用对象,指挥对象做事情。面向对象设计:在管理和维护对象之间的关系。
面向对象特征:
- 封装(encapsulation)
- 继承(inheritance)
- 多态(polymorphism)
例如洗衣服:
- 面向过程:把衣服脱下来–>找一个盆–>放点洗衣粉–>加点水–>浸泡10分钟–>揉一揉–>清洗衣服–>拧干–>晾
起来 - 面向对象:把衣服脱下来–>打开全自动洗衣机–>扔衣服–>按钮–>晾起来
区别:
- 面向过程:分析解决问题的步骤,按照设定好的步骤逐步执行,强调步骤
- 面向对象:分析对象的行为和特征,将每个行为当做一个整体,按照需求进行组合替换,强调的是对象。
例如蛋炒饭与盖浇饭:
蛋炒饭就是饭和蛋一起搅拌均匀,味道均匀,但是如果想要换菜,就需要全部倒掉,重新再做,浪费时间和资源
盖饭就是在饭上面浇灌各种菜,分别做好饭和菜,想吃那种菜就加哪种,如果不想要已经加上的菜,可以倒掉那份菜,换另一种。方便换口味,但是味道不够均匀
面向对象和面向过程的优缺点:
- 面向过程:性能较高,但是高耦合,不易于维护、扩展、复用
- 面向对象:低耦合,使用灵活便于维护,复用性强、可扩展性高,但是性能比不上面向过程,因为调用时需要实例化,需要资源
特点:
面向对象思想是一种更符合我们思考习惯的思想,它可以将复杂的事情简单化,我们可以不用管它具体的实现步骤,将我们从执行者变成了指挥者。
一、类
(一)概述
- Java语言最基本的单位是类,我们将现实世界的事物用一个类来体现。
- 类就是具有相同特征和行为的集合,可以看成是一类事物的模板,是对现实事物的一种抽象描述(如:人、汽车等)
- 对象就是类的实例化,即实际存在的对象、具体的个体(人类中的小明、奔驰汽车等)
- 类的组成
- 属性:指事物的特征,(对象静态特征),例如:小明的眼睛、鼻子等
- 行为:指事物能执行的操作,具有独立功能的代码块组成的整体,使其具有特殊功能的代码集,他是对象的动态行为(即对象的行为,如小明走路、吃饭等)
- 类和对象的关系
- 类:类是对现实生活中一类具有共同属性和行为的事物的抽象描述,是抽象的
- 对象:是看得到摸的着的真实存在的实体,是一类事物中具体的个体,是具体的、真实存在的
简单理解:类是对事物的一种描述,对象则为具体存在的事物,即类是对象的模板,对象是类的实体。
(二)类的定义
类由属性与行为两部分构成:
- 属性:在类中体现为成员变量
- 行为:在类中体现为成员方法
成员变量:定义在类中,在方法体外面的变量
成员方法:对应事物的行为,不能使用static修饰
类的定义步骤:
①定义类
②编写类的成员变量
③编写类的成员方法
public class 类名 {
// 成员变量
变量1的数据类型 变量1;
变量2的数据类型 变量2;
…
// 成员方法 实现功能的代码
方法1;
方法2;
}
示例:
public class test {
//成员变量
String name;
int age;
//成员方法
public void call() {
System.out.println(name + "正在打电话");
}
public void message() {
System.out.println(name + "今年"+ age + "岁了");
}
}
(三)对象
初始化:为元素分配内存空间,并为每个元素赋初始值
引用实际上就是指针。但它是安全的,可控的
创建对象:
类名 对象名 = new 类名();
new 类名(); // 可以不添加对象名,此时是匿名对象
匿名对象应用场景:
(1)对象调用方法仅仅一次的时候
格式:new 类名().方法名(参数...);
好处:匿名对象调用方法后就是垃圾,可以被垃圾回收器回收,从而提高内存的使用效率。
(2)匿名对象可以作为实际参数传递
格式:方法名(new 类名());
注意:匿名对象访问成员变量意义不大,一般不使用。
使用对象访问类中的成员:
对象名.成员变量 对象名.成员方法(参数...);
示例:
public class Test {
//成员变量
String name;
int age;
//成员方法
public void call() {
System.out.println(name + "正在打电话");
}
public void message() {
System.out.println(name + "今年" + age + "岁了");
}
public static void main(String[] args) {
Test t = new Test();
// 调用成员变量
t.name = "小明";
t.age = 18;
// 调用成员方法
t.call();
t.message();
// 若不需要对象名,也可以直接使用对象
new Test().message();
}
}
成员变量的默认值
数据变量 | 默认值 | |
---|---|---|
基本类型 | 整数(byte,short,int,long) | 0 |
浮点数(float,double) | 0.0 | |
字符(char) | \u0000 | |
布尔(boolean) | false | |
引用类型 | 数组、类、接口 | null |
注意:
只有
成员变量
或者类变量
(static修饰的变量)才有默认值,局部变量没有默认值
对象创建过程
创建对象的步骤:
1)加载类信息,只加载一次
2)在堆中给对象分配空间
3)对象初始化操作
a. 默认初始化(赋予默认值:整数:0;小数0.0;字符:\u0000;布尔:false;引用:null);
b. 显示初始化(即赋给成员声明时给到的值)
c. 构造器初始化(把传入构造器的值重新赋给属性,当调用无参构造器时不赋值)
4)返回对象的地址给对象引用;
对象内存图
- 单个数组
- 单个数组修改数据
- 多个数组
- 多个数组指向同个地址
- 方法执行过程
- this内存原理
总结
- 当多个对象的引用指向同一个内存空间(变量所记录的地址值是一样的)。只要有任何一个对象修改了内存中的数据,此后无论使用哪一个对象进行数据获取,都是修改后的数据。
(四)方法
概述:方法是用来实现一个类中某个功能的一段代码
方法的作用:
- 减少代码冗余
- 提高复用性
- 提高可读性
- 提高可维护性
- 方便分工合作
使用经验:一个方法只做一件事,即一个方法只实现一个功能
1、方法的通用格式
public [static] 返回值类型 方法名(参数){
方法体; // 完成某种特定功能的代码
// 当返回类型为void时,return可以不写,或者return后面直接加分号结束
// return:用来结束方法的
return 返回值; // 返回值:功能的返回结果,由return返回给方法的调用者
}
注意:
定义方法时,要做到两个明确
- 明确返回值类型:明确方法操作完毕后是否有返回值,有则写要返回数据的对应数据类型,无则写void
- 明确参数:明确参数类型和数量
调用方法时:
- void类型的方法,直接调用即可
- 非void类型的方法。推荐用变量接收调用
2、参数类型
作用:方法的参数可以让代码功能更灵活、普适性更高,易于修改及维护
参数有形参和实参,定义方法时写的参数叫形参,相当于局部变量的声明,真正调用方法时,传递的参数叫实参,相当于局部变量的赋值。
- 形参:用来接收方法被调用时传递的参数。只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间。因此仅仅在方法内有效。
- 实参:调用中的形参,即使用状态下的形参,传递给被调用方法的值,预先创建并赋予确定值。
不定长参数
就是不限定参数的长度,即可变参数。不定长参数可以理解为数组,使用的时候也
当做数组使用
。
格式:
返回值 方法名(参数类型...参数名)
void s(int... a) // 相当于void s(int [] a)
使用要求:必须放在参数列表的最后面
void s(double d, int... a) // 相当于void s(double d, int [] a)
注意事项和使用细节:
- 可变参数的实参可以为0个或任意多个。
- 可变参数的实参可以为数组。
- 可变参数的本质就是数组。
- 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后。
- 一个形参列表中只能出现一个可变参数。
方法返回值
概念:方法执行后返回的结果
根据需求确定方法有无返回值,返回值类型是什么
什么时候使用teturn语句?
1. 需要一个返回值
2. 符合提前结束方法条件时返回
方法调用
无返回值的方法
直接调用:直接写方法名调用(只有这种调用方式)
public static void main(String[] args) {
print(); // 直接调用
}
public static void print() {
System.out.println("方法被调用");
}
有返回值的方法
直接调用:直接写方法名调用
public static void main(String[] args){
add(5,10); // 一般来说没有意义,所以不推荐
}
public static int add(int num1 , int num2){
int sum = num1 + num2;
return sum;
}
输出语句调用:
在输出语句中调用方法, System.out.println(方法名()) 。
作用不是很大,因为我们不一定非要把返回结果输出,我们可能还需要对返回结果进行进一步的操作。
public static void main(String[] args) {
System.out.println(getSum(5,6));
}
public static int getSum(int a,int b) {
return a + b;
}
赋值调用
调用方法,在方法前面定义变量,接收方法返回值
推荐,因为我们可以使用该变量做其他运算
public static void main(String[] args){
int result = add(5,10);// int result = 15;
System.out.println(result);
//在计算了5+10的总和之后,继续与20进行相加求和
int result2 = add( result , 20);
System.out.println(result2);
}
public static int add(int num1 , int num2){
int sum = num1 + num2;
return sum;
}
return关键字
return的三种用法:
- 应用在具有返回值类型的方法中:
return value;
表示结束当前方法,并伴有返回值,返回到方法调用处。- 应用在没有返回值类型(void)的方法中:
return;
表示结束当前方法,直接返回到方法调用处。- 在判断语句块中,在符合条件时表示结束当前方法,并返回数据
java传递参数
Java传递参数是值传递,所以基本数据类型传值是直接把表达式的值复制给参数,按照引用类型说,就是有两个实体对象,所以你改动其中一个值另一个并不会改变。引用类型传值就是传递引用变量中保存的内存地址值,即它不是直接复制实体对象,而是复制引用变量中的地址,两个引用地址都指向同一个实体对象,操作一个引用在实体对象里面对对象的属性做了改动,另一个引用也能发现这种改动,但是引用本身并不会发生改变,所以引用传值改变的是对象的属性,而不是引用本身。
参考:方法传参
方法参数传递(基本数据类型):形参的改变不影响实参的值
public static void main(String[] args) {
/**
* 输出:
* 方法调用前:100
* 方法调用后:100
*/
int number = 100;
System.out.println("方法调用前:" + number );
change(number);
System.out.println("方法调用后:" + number);
}
static void change(int number){
number = 200;
}
方法参数传递(引用数据类型)
- 将实参的引用类型的值(即在堆空间中生成的首地址的值)传递给形参的引用类型的变量
- 形参方法内的参数值改变时,会影响到实参的值。因为引用类型的对象是存储在堆内存中的,形参和实参都是指向同一个地址,地址内的数据改变了形参和实参也就变了
- 注意:如果形参的地址改变了,那么这个改变不会影响到实参。
- 注意:String类型比较特殊,它是存储在常量池中的,你可以把它近似看成基本数据类型的。因为它的值一旦确定,就无法改变。
总结
- java的基本数据类型是传值调用,引用类型是传引用调用,即java参数之间传递的是变量存储的内容,基本类型存储的是值本身,引用类型存储的内容是对象的地址;
- 当传值调用时,改变的是形参的值,并没有改变实参的值,实参的值可以传递给形参,但是,这个传递是单向的,形参不能传递回实参;
- 当使用引用数据类型作为方法的形参时,若在方法体中修改形参指向的数据内容,则会对实参变量的数值产生影响,因为形参变量和实参变量共享同一块堆区;
- 当使用引用数据类型作为方法的形参时,若在方法体中修改形参变量的指向(地址),此时不会对实参变量的数值产生影响,因为形参变量和实参变量分别指向不同的堆区;
- 当传引用调用时,如果参数是新对象,无论对新对象做了何种操作,都不会改变实参对象,因为两个对象的地址不同,但是如果改变了对象的内容,就会对应改变实参对象的内容。
如:
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
public A(String name) {
this.name = name;
}
public void test(A a){
a = new A("李四");
a.setName("李四");
}
public static void main(String[] args) {
A a = new A("张三");
System.out.println("方法调用前:" + a.getName());
a.test(a);
System.out.println("方法调用后:" + a.getName());
a.test(new A("王五"));
System.out.println("传入新对象后:" + a.getName());
a.test(new A("马六"){
@Override
public void test(A a) {
super.test(a);
System.out.println("匿名内部类的name:" + a.getName());
}
});
System.out.println("传入匿名内部类后:" + a.getName());
}
// 结果:
// 方法调用前:张三
// 方法调用后:张三
// 传入新对象后:张三
// 传入匿名内部类后:张三
- 当传入一个对象或数组时,形参与实参共享一个对象地址,形参修改该对象的属性会影响到实参,因为实参和形参都指向该对象,而该对象的属性被改变,并不会改变该对象的地址
public class TestThread1{
public static void main(String[] args) {
/**
* 输出:
* 方法调用前:10
* 方法调用后:100
*/
F f = new F();
System.out.println("方法调用前:" + f.value );
show(f);
System.out.println("方法调用后:" + f.value );
}
static void show(F f){//改变对象的属性
f.value = 100;
}
}
class F{
int value = 10;
}
值传递机制:
- 基本数据类型:传递值的副本,即复印件,副本改变不会影响原件
- 引用数据类型:传递的也是值的副本,但是引用类型的值指的是对象的地址,原件和复印件都指向同一个地址,所以只要该地址的内容改变了,所有指向该地址的引用都会改变
虚方法
与声明的类无关,由实际创建的对象决定调用哪个类的方法,如:person p=new student(),虽然声明的是person类,但是他实际创建的却是Student对象,所以调用student类的方法。又因为在编译过程是虚的,要在运行过程再去决定调用哪个方法,所以称为虚方法。程序中大部分方法都是虚方法。
三种非虚方法:
1)static方法以声明的类型为准,与实例无关。
static void s(){};
A a=new B();
a.s();// 无论是创建哪个对象,调用的都是同一个是s()
声明的是A类,所以调用的也是A类,与实例new B()无关。
2)private方法:因为子类无法调用,所以也无法虚化。
3)final方法因为无法被覆盖,所以也不能虚化。
方法的注意事项
- 概念:方法执行后的返回结果。
- 不调用则不执行,即方法创建后不会立刻使用,而是等调用者调用才执行(main方法是程序的入口,被JVM自动调用)
- 方法必须先定义,后调用,否则程序将报错
- 方法与方法是平级关系,不能嵌套定义
- 方法定义的时候,参数之间用逗号隔开
- 如果方法有明确的返回值,一定要有return语句,以便返回一个值
- void空返回类型,return可以省略或return后面不能跟数据
- return语句必须放在方法最后一行,除非是放在判断语句块内
代码块
概述:在Java中,使用{}括起来的代码被称为代码块。
分类
分类(根据其位置和声明的不同):
(1)局部代码块
位置:在方法中的局部位置出现
作用:用于限定变量的生命周期(作用域),及早释放空间,提高内存利用率
(2)构造代码块(属于对象成员,存储在堆内存中)
位置:在类中方法外出现
作用:把多个构造方法中的相同代码存放到一起,用于给对象进行初始化
执行特点:每次调用构造方法时都执行,而且在构造方法前执行,每创建一个对象,执行一次代码块
(3)静态代码块(属于类成员,存储在方法区里面的静态区中)
位置:在类中方法外出现,并且在前面加了static修饰符
作用:用于给类进行初始化
执行特点:加载类时执行,并且只执行一次,因此它在构造代码块和构造方法前执行
(4)同步代码块(多线程内容)
(五)方法重载
概述:在同一个类中多个具有相同方法名参数类型或数量、参数顺序不同的方法相互之间构成重载
- 只能在当前类中使用重载的方法
- 方法重载与返回值无关,只与方法名、参数列表有关
- 调用重载方法时,首先通过方法名定位,有相同方法名时,优先调用与参数类型一致的方法,当没有与传递参数类型一致的方法时,再去找可以进行转化的方法
- 在调用时,JVM通过参数列表的不同来区分同名方法
- 好处:灵活、方便、屏蔽使用差异。
弊端:每一个方法与某一种具体类型形成了密不可分的关系,耦合太高
注意:
- 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
- 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载
方法重载练习
需求:使用方法重载的思想,设计比较两个整数是否相同的方法,兼容全整数类型(byte,short,int,long)
思路:
①定义比较两个数字的是否相同的方法compare()方法,参数选择两个int型参数
②定义对应的重载方法,变更对应的参数类型,参数变更为两个long型参数
③定义所有的重载方法,两个byte类型与两个short类型参数
④完成方法的调用,测试运行结果
public static void main(String[] args) {
//调用方法
System.out.println(compare(10, 20));
System.out.println(compare((byte) 10, (byte) 20));
System.out.println(compare((short) 10, (short) 20));
System.out.println(compare(10L, 20L));
}
//int
public static boolean compare(int a, int b) {
System.out.println("int");
return a == b;
}
//byte
public static boolean compare(byte a, byte b) {
System.out.println("byte");
return a == b;
}
//short
public static boolean compare(short a, short b) {
System.out.println("short");
return a == b;
}
//long
public static boolean compare(long a, long b) {
System.out.println("long");
return a == b;
}
Java遵循就近原则,会自动寻找最符合的类型
public static void print(Object obj){
System.out.println("Object");
}
public static void print(String str){
System.out.println("String");
}
public static void main(String[] args) {
print(null);//输出String
}
(六)构造方法
构造方法:类中的特殊方法,主要用于初始化对象,给对象的成员变量赋初始值。
public class 类名{
修饰符 类名( 参数 ) {
}
}
特点:
- 名称与类名完全相同。
- 没有返回值类型,也不能写void。
- 创建对象时,触发构造方法的调用,不可通过
.
手动调用。
注意:如果没有在类中显示定义构造方法,则编译器默认提供无参构造方法。如果手动定义了构造方法,则系统不再提供默认构造方法,可根据需求自行添加。
- 构造器(构造方法)是用来初始化对象化的,并不是用来创建对象的
- 构造方法不能被继承,因为构造方法的名字必须与类名一致,但是可以被调用
构造方法重载
构造方法也可重载,遵循重载规则
创建对象时,根据传入参数,匹配对应的构造方法
构造方法为属性赋值
- 创建对象的同时,将值传入构造方法
- 由构造方法为各个属性赋值
(七)this关键字
- 类是模板,可服务于此类的所有对象;
- this是类中的默认引用,代表当前实例;
- 当类服务于某个对象时,this则指向这个对象;
- this不能在类定义的外部使用,只能在类定义的方法中使用。
this的本质就是已创建对象的地址,代表当前对象
记住 :方法被哪个对象调用,方法中的this就代表那个对象。即谁在调用,this就代表谁
this的三种用法:
- 访问字段和方法。
this.成员变量名 调用本类的成员变量 this.成员方法名(); 调用本类的成员方法
- 区分字段和局部变量。
public class Test{ private String name = "张三"; public static void main(String[] args){ String name = "李四"; System.out.println(name); // 输出李四 System.out.println(this.name); // 输出张三 } }
- 在构造方法中调用本类的其他构造方法(必须写在构造方法的第一句,且只能调用一个,一般用参数少的构造方法调用参数多的方法,没有的参数直接给默认值,这样方便代码阅读),
注意:只能在构造方法中使用,且只能写在第一行
。
- this():调用无参构造
- this(实参):调用有参构造
- 目的:减少重复代码
class Teacher{
String name;
int age;
double salary;
public Teacher() {
System.out.println("无参构造方法执行完毕");
}
/*
public Teacher(String name) {
this.name = name; // 重复写赋值语句,代码冗余且麻烦
}
public Teacher(String name , int age) {
// 重复写赋值语句,代码冗余且麻烦
this.name = name;
this.age = age;
}
*/
public Teacher(String name) {
// 默认this();即默认调用本类无参构造器
//this.name = name;
this(name,0,0.0);//调用带三个参数的构造方法
}
public Teacher(String name , int age) {
//this.name = name;
//this.age = age;
this(name,age,0.0);//调用带三个参数的构造方法
}
public Teacher(String name , int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
}
(八)super关键字
- super关键字代表父类存储空间的标识(可以理解为父类引用,借此操作父类的成员)。
- 用法跟this关键字差不多,表示调用父类的成员。访问时将忽略本类,直接从父类开始查询,即就算子类重写了该成员,也不会被使用,只会使用父类的成员
可以看成直接父类对象的引用,用来访问被子类覆盖的直接父类的方法和属性
super的三种用法:
1、访问父类的方法和字段。
super.成员变量名 调用父类的成员变量 super.成员方法名(); 调用父类的成员方法
2、区分子类与父类中同名的字段和方法。
class Son extends Father{ // 父子类的同名属性不存在重写关系,两块空间同时存在(子类遮蔽父类属性),需使用不同前缀进行访问。 int field = 20; public void run(){ System.out.println("子类重写了父类的跑步方法"); super.run(); // 访问父类的run()方法 } public void method() { System.out.println(super.field);//在子类中访问父类的field属性 System.out.println(this.field); // 访问本类的field属性 } public static void main(String[] args) { Son son = new Son(); son.method(); son.run(); // 访问本类的run()方法 } }
3、在构造方法中调用父类的构造方法(必须写在构造方法的第一句)。
super():表示调用父类无参构造方法。
super(实参):表示调用父类有参构造方法。
class Father{
int field;//父类提供的属性
public Father() {
System.out.println("Father() - 无参构造被执行");
}
public Father(int a) {
this.field = a;
System.out.println("Father(int a) - 有参构造被执行");
}
}
class Son extends Father{
public Son() {
//super(); 没显式声明调用时,默认调用父类无参构造
System.out.println("Son() - 无参构造被执行");
}
public Son(int b) {
super(b);
System.out.println("Son(int b) - 有参构造被执行");
}
public Son(double c) {
super(1);
System.out.println("Son(double c) - 有参构造被执行");
}
public static void main(String[] args) {
new Son();
System.out.println("-------------");
Son son = new Son(10);
System.out.println("-------------");
new Son(3.5);
System.out.println(son.field);
}
}
(九)this和super
- this:调用本类的属性和方法,包括继承来的。
从本类开始查找属性和方法,没有则查找父类
- this():调用本类的构造方法
- super:调用父类的属性和方法。
忽略本类,直接从父类开始查找属性和方法
- super():调用父类的构造方法。
- this()和super()都不能在static环境中使用,包括static变量,static方法,static语句块。
- this()和super()不能在同一个构造方法中使用,this()和super()有且只能有一个,因为他们都需要放在构造方法的第一行,如果this()和super()都不写,则默认调用父类的无参构造方法,因为在子类的构造方法中会默认添加一个super(),前提是父类必须拥有无参构造方法,方法里面可以什么都没有。
注意:当父类无参和有参构造方法都具备的时候,默认调用无参构造方法,即当没有表明时,在子类构造方法的第一句默认添加super(),也可以显式的声明调用有参构造方法super(参数列表),子类构造方法可以随意调用父类的有参、无参构造方法;但是,当父类只有有参构造方法而没有无参构造方法时,必须在子类构造方法的第一句显式的声明super(参数列表)
this和super都指的是同一个对象
this和super都指的是同一个对象,只是在内存中的查找顺序不同,this是从本类开始查找,super是从父类开始查找
public class Test extends Car {
public static void main(String[] args) {
new Test().p();
}
public void p(){
System.out.println(this.getClass()); // 输出 class Test
System.out.println(super.getClass()); // 输出 class Test
}
}
class Car {}
面向对象-三大特性
面向对象的三大特性是
封装、继承、多态
访问修饰符
修饰符 | 作用 |
---|---|
private | 私有的,只能在本类中调用 |
default | 默认的(可以省略不写),只能在同一个包中调用 |
protected | 允许在同一个包中或其他包中的子类调用 |
public | 开放的,在哪都可以调用 |
作用范围
作用域 | 本类 | 同包 | 不同包下的子类 | 全部地方 |
---|---|---|---|---|
private | √ | |||
default | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
注意:父子类都在一个包中,子类可以访问父类的protected成员,在子类中创建一个父类对象时,父类对象也可以访问protected成员
父子类不在同一个包中,子类可以访问父类的protected成员,在子类中创建一个父类对象时,父类对象不可以访问protected成员
一、封装
为什么要封装?
- 直接在对象的外部为对象的属性赋值,并没有对属性的赋值加以控制,就可能存在非法数据的录入。
- 封装作用:
1)减少耦合
2)类内部的结构可以自由修改
3)对其成员进行更精确的控制
4)隐藏信息,实现细节
一)什么是封装?
概述:隐藏对象的属性和内部实现细节,仅对外提供公共访问方法,控制对象的修改及访问的权限。
二)封装的原则
- 将不需要对外提供的内容都隐藏起来
- 属性一般用private修饰
私有属性由get/set方法进行读取和赋值操作,boolean类型的get方法改为is开头
- 要访问被封装的私有属性,必须通过set、get方法
- 除了只在本类使用的方法用private修饰外,其他方法都使用public修饰
三)封装的实现步骤
(1)用private关键字把属性设置为私有
(2)提供公共的setter和gette方法,用public修饰
(3)通过setter方法对输入的数据进行过滤
小提示:调用get、set方法时,get方法可以直接放在输出语句中,set不可以
在公共的访问方法内部,添加逻辑判断,进而过滤掉非法数据,以保证数据安全。
public class TestEncapsulation {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "tom";
//s1.age = 20;//安全,无法直接访问到私有属性
s1.setAge(20);//调用方法完成赋值操作
s1.sex = "male";
s1.score = 99.0;
System.out.println( s1.getAge() );//调用方法完成取值操作
}
}
class Student{
String name; //属性也称为字段
private int age;//私有属性,本类可见,其他位置不可见
String sex;
double score;
public Student() {}
/**
* 为age属性赋值的方法
*/
public void setAge(int age) {
if(age >= 0 && age <= 153) {//合法区间
this.age = age;
}else {
this.age = 18;//如果录入的数据不合法,默认使用18代表用户的年龄
}
}
/**
* 为age属性取值的方法
*/
public int getAge() {
return this.age;
}
}
如何防止构造器破坏封装
当我们定义了有参构造器时,别人可以直接通过构造器进行赋值,从而绕过set方法的校验
public class TestEncapsulation {
public static void main(String[] args) {
Student s1 = new Student();
//s1.name = "tom";//,无法直接访问到私有属性
System.out.println( s1.getAge() );//输出18
// 但是可以通过构造器直接为属性赋值,此时将绕过set方法的校验
Student s2 = new Student("asdkjsahdkasjdkasj",10000);
System.out.println( s2.getName() + s2.getAge() );//输出 asdkjsahdkasjdkasj10000
}
}
class Student{
private String name;
private int age;
public Student() {}
public Student(String name,int age) {
this.name = name;
this.age = age;
}
/**
* 为name属性赋值的方法
*/
public void setName(String name) {
if(name.length() >= 2 && name.length() <= 10) {//名字长度在2-10个字符之间
this.name = name;
}else {
this.name = "java";
}
}
/**
* 为name属性取值的方法
*/
public int getName() {
return this.name;
}
/**
* 为age属性赋值的方法
*/
public void setAge(int age) {
if(age >= 0 && age <= 150) {//合法区间
this.age = age;
}else {
this.age = 18;//如果录入的数据不合法,默认使用18代表用户的年龄
}
}
/**
* 为age属性取值的方法
*/
public int getAge() {
return this.age;
}
}
针对声明了有参构造器的类,我们可以通过以下方式进行封装:
在有参构造器中调用set方法,不直接使用属性进行赋值,防止其绕过set方法的校验
public class TestEncapsulation {
public static void main(String[] args) {
Student s1 = new Student();
//s1.name = "tom";//,无法直接访问到私有属性
System.out.println( s1.getAge() );//输出18
// 因为有参构造器内部调用set方法为属性赋值,所以要遵守set方法的校验规则
Student s2 = new Student("asdkjsahdkasjdkasj",10000); // 该数据不合理,进行默认赋值
System.out.println( s2.getName() + s2.getAge() );//输出java18
}
}
class Student{
private String name;
private int age;
public Student() {}
public Student(String name,int age) {
// 在构造器中调用set方法
setName(name);
setAge(age);
}
/**
* 为name属性赋值的方法
*/
public void setName(String name) {
if(name.length() >= 2 && name.length() <= 10) {//名字长度在2-10个字符之间
this.name = name;
}else {
this.name = "java";
}
}
/**
* 为name属性取值的方法
*/
public int getName() {
return this.name;
}
/**
* 为age属性赋值的方法
*/
public void setAge(int age) {
if(age >= 0 && age <= 150) {//合法区间
this.age = age;
}else {
this.age = 18;//如果录入的数据不合法,默认使用18代表用户的年龄
}
}
/**
* 为age属性取值的方法
*/
public int getAge() {
return this.age;
}
}
二、继承
概述:多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
定义:子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。
- 两个类之间的继承关系,必须满足“is a”的关系。
可以使用IDEA中的类图来查看类之间的关系
IDEA类图的字段与属性的区别
- 字段:就是平时所说的成员变量(属性)
- 属性:特指以set或get开头的方法,提取set或get后面的名称当做属性
一)继承的作用
继承设计思想:
父类构造器完成父类属性的初始化
子类构造器完成子类属性的初始化
好处:强代码的复用性,更容易实现类的扩展(多个类相同的成员可以放到同一个类中)
提高了代码的可维护性(如果某功能的代码需要修改,只需修改一处即可),方便对事务建模
让类与类之间产生了关系,是多态的前提,
这里其实也是继承的一个弊端:类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性
开发的原则:低耦合,高内聚
- 耦合:类与类的关系
- 内聚:自己完成某件事情的能力
二)父类的选择
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,这个类就是父类,基类或者超类
- 功能越精细,重合点越多,越接近直接父类。
- 功能越粗略,重合点越少,越接近Object类。(万物皆对象的概念)
public class TestExtends {
public static void main(String[] args) {
Dog dog1 = new Dog();
dog1.breed = "哈士奇"; //继承自父类
dog1.age = 2; //继承自父类
dog1.sex = "公"; //继承自父类
dog1.furColor = "灰白"; //子类独有的
dog1.eat(); //继承自父类
dog1.sleep(); //继承自父类
dog1.run(); //子类独有的
//------------------------------------------
Bird bird1 = new Bird();
bird1.breed = "麻雀";
bird1.age = 1;
bird1.sex = "雄";
bird1.furColor = "棕";
bird1.eat();
bird1.sleep();
bird1.fly();
}
}
//父类
class Animal{
String breed;
int age;
String sex;
public void eat() {}
public void sleep() {}
}
class Dog extends Animal{ //完成继承关系
String furColor;
public void run() {}
}
class Bird extends Animal{
String furColor;
public void fly() {}
}
class Fish extends Animal{
public void swim() {}
}
class Snake extends Animal{
public void climb() {}
}
三)声明格式
// Java通过extends关键字可以实现类与类之间的继承
// 产生继承关系之后,子类可以使用父类中的属性和方法,也可定义子类独有的属性和方法
class 子类名 extends 父类名{}
四)继承的特点
- java是单继承,一个子类只能有一个直接父类,但是可以多层级间接继承其他父类的属性和方法,一个父类可以有多个子类(使用接口可以实现多继承)
- 子类只能继承父类所有非私有成员(成员方法和成员变量),但是可以通过非私有成员访问父类的私有成员,
这里其实也体现了继承的另一个弊端:打破了封装性。所以,不要为了部分功能而去继承
- 子类不能继承父类的构造方法(
构造方法名必须与类名一致,继承之后子类名与父类构造方法名称不一致
),但是可以通过super关键字去访问父类的构造方法- Java中所有类的父类都是Object类,即如果不使用extends标明继承的直接父类,则系统默认该类的直接父类是java.lang.Object类
- 有些语言是支持多继承的,格式:class 子类名 extends 父类名1,父类名2…{}
五)方法重写(override)
思考:为什么需要在子类中定义和父类相同的方法?
当父类提供的方法无法满足子类需求时,可在子类中定义和父类相同的方法进行重写(Override)。
概述:子类中出现了和父类中一模一样的方法声明的动作称为方法重写,也被称为方法覆盖,方法复写。
重写是实现多态的必要条件
,子类通过重写父类方法,用自身行为替换父类行为。
方法重写原则(两同两小一大):
1)两同 :子类方法名和形参列表必须与父类的一致
2)两小:子类方法的返回值类型和声明异常类型要小于等于父类方法(
子类方法的 返回值类型和声明异常类型 必须是父类方法的 返回值类型和声明异常类型 或者 是其返回值类型和声明异常类型的子类型
)3)一大:子类方法的访问权限的范围要大于或等于父类方法
方法重写与重载的区别
名称 | 发生范围 | 方法名 | 形参列表 | 返回值类型 | 异常声明类型 | 修饰符 |
---|---|---|---|---|---|---|
重载(overload) | 本类 | 必须一样 | 类型、个数或顺序至少有一个不同 | 无要求 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 必须一样 | 子类方法的返回值类型必须和父类方法的返回值类型一致,或者是父类方法返回值类型的子类型 | 子类方法的声明异常类型必须和父类方法的声明异常类型一致,或者是父类方法声明异常类型的子类型 | 子类方法的访问权限的范围要大于或等于父类方法 |
六)继承中的成员关系
查找顺序(就近原则):
子类成员和父类成员名称不同时,根据执行顺序查找
子类成员和父类成员名称相同时:
/**
* 首先在子类方法的局部范围查找
* 然后在子类成员范围查找
* 接着在父类成员范围查找(不能访问父类的局部范围)
* ...
* 最后还是没有找到,就报错
* 若是找到了,但是不符合访问规则,如直接访问父类私有成员等,也会报错
* 示例:
*/
class Fu {
// Fu中的成员变量。
int num = 5;
}
class Zi extends Fu {
// Zi中的成员变量
int num = 6;
public void show() {
// 访问父类中的num,但是由于子类属性重名,导致父类属性值被覆盖,若使用super.num则会直接输出父类属性的值
System.out.println("Fu num=" + num);
// 访问子类中的num
System.out.println("Zi num=" + num);
}
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}
演示结果:
Fu num = 6
Zi num = 6
为什么名称相同时先查找子类成员?
因为java中首先执行的是父类的成员,其次才是子类的成员,又由于子类成员与父类成员的名称相同,在子类进行访问时,父类成员的值会被子类所覆盖(
使用super直接访问除外
),所以先查找父类成员没有意义,只有在子类中没有查到该成员时,才会去父类查找
使用super访问父类成员时,要遵循封装原则,不能直接访问父类私有成员,可以借助父类暴露出来的访问接口来进行间接访问,如通过getXxx方法和setXxx方法等
七) 继承中的构造方法
父子类的初始化(分层初始化):
先进行父类的初始化,再进行子类的初始化。即加载类信息时先加载父类的信息,再加载子类的信息
子类中所有的构造方法默认都会访问父类中的无参构造方法
子类中每一个构造方法的第一条语句默认都是:super()
子类会继承父类中的属性,所以,子类在初始化之前,一定要先完成父类属性的初始化(
没有父类成员,子类何谈继承
)。
子类通过super(…)显式调用父类的带参构造方法时,将不再访问父类的无参构造方法
注意:子类不能多次调用父类的构造方法
如果父类中没有无参构造方法,程序在编译时会报错。如何解决呢?
1)在父类中添加一个无参构造方法
2)子类通过super(参数)去显式调用父类其它的带参构造方法
3)子类通过this(参数)去调用本类的其它构造方法,而本类的其它构造方法也必须首先访问了父类的构造方法
注意:
- 构造方法之间的调用只能调用一次
- super(…)或者this(…)必须出现在第一条语句上,否则,就会对父类的数据进行多次初始化。
- super(…)和this(…)不能同时出现在子类的同一个构造方法中。
八)instanceof关键字
概述:instanceof是一个关系运算符,用来判断一个对象的运行类型是不是该类或其子类(
只能用于判断具有继承关系的类,两个无关的类不能使用instanceof判断
)
格式:
对象名 instanceof 类名
例: student instanceof Person
类型判断:判断 student 对象的运行类型是否是Person类型或Person的子类型,如果两个对比的对象不存在直接和间接的继承关系,那么将不能编译通过;如果student 不是Person类型对象或Person的子类型对象,将返回false。
三、多态
概述:某一个对象(事物),在不同时刻表现出来的不同状态(或者说:同一个方法由于调用对象的差异导致不同的行为),
注意:多态是方法的多态,而不是属性的多态,即多态与属性无关
如学生对象在不同时刻的身份:
1、多态的前提:
1)存在继承关系
2)父类方法被子类方法重写
3)父类引用指向子类对象:
父类 引用名 = new 子类();
2、多态的利弊
好处:
一致的类型:所有子类对象都可以当作共同(共有)的父类对象来处理
多态的优势主要提现在向上转型
提高了程序的可扩展性(由多态保证)
- 使用父类作为方法形参实现多态,使方法参数的类型更为宽泛
- 使用父类作为方法返回值实现多态,使方法可以返回不同子类对象
提高了程序的可维护性(由继承保证)
弊端:
不能访问子类特有的方法。因为编译的时候主要看左边的类型,左边是父类型,所以不能访问子类型特有的方法。
3、多态的转型
前面提到了,多态有一个弊端,那就是不能访问子类特有的方法,这里可以利用向下转型解决这个问题
向上转型
概述:声明父类引用指向子类对象,属于自动类型转换,
父类 引用名 = new 子类();
注意:
向上转型只能调用父类拥有的属性和方法
。在调用成员方法和成员变量进行编译时,编译器默认其为声明的类型,即声明什么类型的对象引用,则编译器就认为该对象是此类型。如:
Animal a = new cat(); // 编译器默认new 出来的是Animal类型,也只能调用Animal类的成员
但是:编译完成后,在实际执行中,调用成员变量时,还是调用Animal类的成员变量(
因为多态与属性无关,所以无论是编译还是运行阶段都只看声明的类型,即等号左边的类型
),调用成员方法时,默认会调用Cat类中重写Animal类的方法,即调用成员方法时,由于父类方法被子类同名方法所覆盖,所以调用的是子类的同名方法;若Cat类没有重写该方法,则在Animal类中查找,其遵循继承的查找规则。编译看左边,执行看右边,等号左边是编译类型,右边是运行类型
多态中的成员访问特点:
(1)成员变量
编译看左边,执行也看左边。
成员变量的访问是就近原则。声明的是父类的引用,所以,访问的是父类的成员变量。
(2)构造方法
创建子类对象的时候,访问父类的构造方法,对父类的数据进行初始化。
(3)成员方法
编译看左边,执行看右边
- 这里存在java的动态绑定机制:当调用对象方法的时候,
该方法回合该对象的内存地址/运行类型
绑定。所以非静态的成员方法始终跟着对象走。创建的是子类对象,则调用的也是子类的成员方法。
public class Test{
public static void main(String[] args){
Person p = new Son();
/* 动态绑定机制:虽然声明的是Person类,但是实际上指向的却是Son对象。
由于Son类重写了sum方法,所以调用的是Son对象的sum方法,又由于属性没有多态,
只遵循就近原则,哪里声明就在哪里调用,所以输出150
*/
System.out.println(p.sum());
/* 动态绑定机制:虽然声明的是Person类,但是实际上指向的却是Son对象。
由于Son类重写了sum1方法,所以调用的是Son对象的sum1方法,又由于属性没有多态,
只遵循就近原则,哪里声明就在哪里调用,所以输出120
*/
System.out.println(p.sum1());
// 声明的是Person类,所以调用的是Person类的 i属性,输出10
System.out.println(p.i);
}
}
class Person{
public int i = 10;
public int sum(){
return i + 50;
}
public int sum1(){
return getI() + 50;
}
public int getI(){
return i;
}
}
class Son{
public int i = 100;
public int sum(){
return i + 50;
}
public int sum1(){
return i + 20;
}
public int getI(){
return i;
}
}
当子类只重写了个别方法时:
public class Test{
public static void main(String[] args){
Person p = new Son();
/* 动态绑定机制:虽然声明的是Person类,但是实际上指向的却是Son对象。
由于Son类没有重写sum方法,所以调用的是Person类的sum方法,又由于
属性没有多态,只遵循就近原则,哪里声明就在哪里调用,所以输出60
*/
System.out.println(p.sum());
/* 动态绑定机制:虽然声明的是Person类,但是实际上指向的却是Son对象。
由于Son类没有重写sum1方法,所以调用的是Person类的sum1方法,又由于
Son类重写了getI方法,且实际指向的是Son对象,所以调用Son对象的
getI方法,getI方法返回i属性,又由于属性没有多态,只遵循就近原则,
哪里声明就在哪里调用,输出150
*/
System.out.println(p.sum1());
// 声明的是Person类,所以调用的是Person类的 i属性,输出10
System.out.println(p.i);
}
}
class Person{
public int i = 10;
public int sum(){
return i + 50;
}
public int sum1(){
return getI() + 50;
}
public int getI(){
return i;
}
}
class Son{
public int i = 100;
public int getI(){
return i;
}
}
(4)静态成员(静态变量和静态方法)
编译看左边,执行也看左边。
静态成员与类相关。声明的是父类的引用,所以,访问的是父类的静态成员。
结论:由于成员方法存在方法重写,所以它在执行的时候看右边。
向下转型
声明子类引用指向父类,属于强制类型转换(
可调用子类的方法和属性
)
要求父类引用必须能够转换为子类引用,否则将会报:ClassCastException:类型转换异常
子类 引用名 = (子类)父类名;
向下转型使用场景
- 要对子类做特殊处理时才使用向下转型
- 向下转型后才可以调用子类特有的成员
注意:如果进行向下转型的对象本来就是父类型,那么转换为子类型时编译不会报错,但是运行时会报错
如:
class Father{}
class Daughter extends Father{}
class Son extends Father{
public static void main(String[] args){
Father fa = new Son(); // 编译通过
Son son = (Son)fa; // 运行不会报错,因为fa指向的本来就是Son类型对象
Father f = new Father();//本来就是父类型
//进行向下转型后,编译不报错,运行报错,因为运行时会找到实际创建的对象,而这里实际创建的是Father类型,强行转换成Son类型就会报错
Son s = (Son) f ;
Father father = new Son();
//进行向下转型后,编译不报错,运行报错,因为运行时会找到实际创建的对象,而这里实际创建的是Son类型,与Daughter无关,强行转换成Daughter类型就会报错
Daughter d = (Daughter) father ;
}
}
编译器只能识别声明的类型,声明父类编译器就认为该类型为父类型,只能调用父类拥有的子类方法,但是本质上还是子类的对象;声明父类引用指向子类对象之后,也可以将该引用强制转换成另一个子类的引用,编译器不会报错,但是因为本质上是第一个子类的对象,两个子类并不相同,所以运行时会报类型转换错误
四、static关键字
概述:static关键字可以用来修饰成员变量和成员方法,表示静态的
静态就是创建一个程序共有的成员,只要静态对象修改了,所有调用该静态的引用都会随之改变;它是类的,不属于任何一个实例对象,它不是存储在任何一个对象的空间,而是存储在类的空间,它可以通过类名或对象名调用;
对象的方法在内存中有专用的代码段,而类方法却没有
静态修饰的内容我们一般称其为:与类相关的,即
类成员
;而非静态的,称其为:对象成员
。被static修饰的成员存储在方法区中的
静态区
中
方法区的分区:类文件区、静态区、方法代码区、常量池
一)特点
随着类的加载而加载
类加载时机: (1)创建对象(包括创建子类对象,即任意对象)。 (2)访问静态属性/静态方法。 (3)主动加载:Class.forName(“全限定名”);。
优先于对象存在。所以,可以被所有对象共享。
被类的所有对象共享,在全类中只有一份,不因创建多个对象而产生多份。(这也是我们判断是否使用静态关键字的条件)
可以通过类名直接调用(其实它本身也可以通过对象名调用,推荐使用类名调用)
通过类名调用: 类名.静态成员变量名; 类名.静态成员方法名(); 通过对象名调用(不推荐): new 类名().静态成员变量名; new 类名().静态成员方法名();
二)静态分类
1、静态代码块
静态语句块用来初始化类,构造方法用来初始化对象
定义在成员位置,随着类的加载而执行且最多只执行一次,优先于main方法和构造方法的执行。
格式:
static { 内容 };
2、静态属性
当 static 修饰成员变量时,该变量称为类变量(类属性,静态变量)
该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。
格式:
static 数据类型 变量名;
3、静态方法
当 static 修饰成员方法时,该方法称为类方法(静态方法) 。静态方法在声明中有 static ,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单.
修饰符 static 返回值类型 方法名 (参数列表){
// 执行语句
}
三)static注意事项
静态方法只能调用静态变量、其他静态方法,不能使用this和super关键字,因为这两个关键字代表的是当前对象和父类对象,而静态只属于类,当类存在的时候,静态也存在,对象不一定存在,所以不能有这两个关键字或者其他属于对象的变量和方法
静态成员只能访问静态成员,非静态成员可以访问静态和非静态的成员
静态方法与非静态方法的区别:在编译期间静态方法的行为就已经确定,而实例方法只有在运行期间当实例确定之后才能确定,也就是说静态方法一开始的行为就确定了,而实例方法要等创建对象才能知道。
静态语句块、属性、方法只在类加载时执行且仅执行一次
,就是说,无论创建了几个对象,静态代码块、属性、方法永远都是只执行一次。什么时候定义静态成员?
(1)什么时候定义静态属性
如果是每个对象都需要使用到的属性,就定义为静态属性
(2)什么时候定义静态方法
如果这个方法中没有调用非静态的数据,就可以定义为静态方法。静态方法一般用于一些工具类的开发。
四)静态变量和成员变量的区别
(1)所属不同
静态变量属于类,所以也称为类变量
成员变量属于对象,所以也称为实例变量(对象变量)
(2)存储在内存中位置不同
静态变量存储于方法区的静态区
成员变量存储于堆内存
(3)生命周期
静态变量随着类的加载而加载,随着类的消失而消失
成员变量随着对象的创建而存在,随着对象的消失而消失
(4)调用不同
静态变量可以通过类名调用,也可以通过对象名调用
成员变量只能通过对象名调用
五、java中个语句的执行顺序
1)父类静态属性和静态代码块初始化(优先级相同):到底是静态属性先执行还是静态代码块先执行,要看其声明的顺序,声明顺序在先的先执行
2)子类静态属性和静态代码块初始化:先声明先执行
3)main方法
4)父类代码块
5)父类构造方法
6)父类普通方法
7)子类代码块
8)子类构造方法
9)普通方法
普通代码块与普通属性初始化的优先级相同
注意:父类的成员一定是先于子类成员执行的
,如:父类静态属性和静态代码块一定是最先执行的,然后才到子类的静态属性和静态代码块
,再到main方法,父类构造器/代码块/普通静态方法/实例方法、子类构造器/代码块/普通静态方法/实例方法,这几个的执行顺序主要是看在main方法中被调用的顺序,一般先被调用的先执行,但是,在父类构造器/代码块和子类构造器/代码块这四者之间的执行顺序是固定的,必定先执行父类普通代码块,再执行父类构造器,然后到子类普通代码块,然后才是子类构成器
构造方法与静态语句块的执行顺序
1)执行静态语句块(因为要先把类信息加载出来,才能构建对象,先有设计图纸,才有具体物品
),向上追溯
到Object类的静态语句块,一层一层执行下来,直至到本语句块,即如果继承了Object类,则必须先执行Object类的静态语句块,然后才执行本语句块
2)执行构造方法(执行顺序与静态语句块一致
),因为构造方法第一句默认super(),会向上追溯
到Object类
六、final关键字
概述:final关键字是一个修饰符,表示最终的,它可以用来修饰类、方法以及变量。
final特点
1)final修饰的变量将成为常量,无初始值
,必须对其赋值且只能赋值一次
格式:
基本类型:
fianl 基本类型 常量名 = 字面值常量;
public static fianl 基本类型 常量名 = 字面值常量; (此处只能是成员变量)
引用类型(对象):
fianl 引用类型 常量名 = 常量值;
public static fianl 引用类型 常量名 = 常量值; (此处只能是成员变量)
对象引用所指向的地址值不能发生改变,但对象的属性值可以发生改变。
基本类型:其值不能发生改变
引用类型:其地址值不能发生改变,但是,该对象的堆内存中的数据值是可以改变的
2)final修饰的方法不可以被子类重写,但可以重载、继承
格式: 修饰符 final 返回值类型 方法名(参数列表){ //方法体 }
3)final修饰的类是最终类,不可被继承,没有子类
格式: final class 类名 {}
注意:final修饰变量分以下几种情况:
1)局部常量:修饰局部变量时,可以不进行初始化操作,但必须且只能赋值一次后才能被使用
public static void main(String[] args) {
// 声明变量,使用final修饰
final int a;
// 第一次赋值
a = 10;
// 第二次赋值
a = 20; // 报错,不可重新赋值
// 声明变量,直接赋值,使用final修饰
final int b = 10;
// 第二次赋值
b = 20; // 报错,不可重新赋值
}
2)实例常量:
-
修饰实例变量时,可以声明变量的同时对其赋值
public class Test { final String USERNAME = "张三"; private int age; }
-
可以先声明然后在构造方法中赋值,在构造方法中进行赋值时,类中所有构造方法都应对其进行赋值
public class Test { final String USERNAME ; private int age; public Test(String username, int age) { this.USERNAME = username; this.age = age; } public Test(String username) { this(username,0); } public Test() { this.USERNAME = "张三"; } }
-
也可以在代码块中进行赋值,但是有多个代码块时只能选择其中之一进行赋值
public class Test {
final String USERNAME ;
{
this.USERNAME = "张三";
}
// {
// this.USERNAME = "张三"; //
// }
}
3)静态常量
-
修饰静态变量时,可以声明变量的同时对其赋值
public static final String USERNAME = "张三" ;
-
可以先声明然后在静态代码块中赋值,且多个静态代码块中只能有一个对静态常量进行赋值
public class Test {
public static final String USERNAME;
static {
USERNAME = "张三" ;
}
// static {
// USERNAME = "张三" ;
// }
}
4)对象常量:修饰引用类型变量时,引用类型变量地址不可以改变,但是该变量的内容可以改变,对象常量也是实例常量的其中一类
public static void main(String[] args) {
// 创建 Test 对象
final Test t = new Test();
// 调用setName方法
t.setName("张三"); // 可以修改
// 创建 另一个 User对象
t = new Test(); // 报错,指向了新的对象,地址值改变。
}
5)被final修饰的参数在本方法中不可以被赋值。
public class Test {
public String USERNAME;
public static double area(final double PI,double r) {
return r * r * PI;
}
public void test(final Test name) {
System.out.println(getClass()); // class Test
name.USERNAME = "张三";
// name = new Test(); // 报错,指向了新的对象,地址值改变。
System.out.println(getClass()); // class Test
}
public static void main(String[] args) {
double area = area(3.14, 5);
System.out.println(area);
new Test().test(new Test());
}
}
声明 final 方法的主要目的是防止该方法的内容被修改。