第七章认识面向对象
1.什么是对象?
在Java中,对象(Object)是指一个具体事物的实例,任何事物都可以使用对象(类)来描述,如猫、狗、计算机、杯子、云、水、空气、叶子、灰尘等看得见的、看不见的、宏观的、微观的、具体的、抽象的都是对象,总之"万物皆对象";
对象是实际存在的个体。(真实存在的个体)
宋小宝就是一个对象
姚明就是一个对象
刘德华就是一个对象
…宋小宝、姚明、刘德华这3个对象都属于“明星”。
2.面向对象的程序设计
Java是一门面向对象的编程语言,面向对象是一种程序设计思想,与之对应的还有面向过程程序设计;面向对象是把一个对象的特征(属性)和行为单独封装到对象源代码中;这些属性和行为都被集中到一个地方,这样比把方法或者过程与数据分散开来更为方便和安全,含义更加明确;
3.面向对象和面向过程
举例子:开车去上班
- 面向过程:去车库提车、拿钥匙、打火、踩离合、挂挡、踩油门、刹车、加油、到公司、找车库停车
- 面向对象:打个滴滴(对象)、到公司
可以看得出来,面向过程关注的是步骤,将所有步骤连在一起"我就能到公司上班",面向对象则关注的是"开车去上班"这个事物整体,完成这件事的步骤全部封装起来,交给指定的对象去做(司机);
tips:也是个人的理解
面向过程主要关注的是:实现步骤以及整个过程。过程A-B-C 这个三个过程都有因果关系.
如果这三个过程中有一个过程发生错误,
就如上述例子一样,如果去车库提车,如果车库钥匙丢了呢?那么整个过程无法进行面向对象主要关注的是:对象A,对象B,对象C,然后对象ABC组合,或者CBA组合…
这单个对象或者是对象的组合完成一个特定的功能
如果这其中一个对象发生了错误,那么就可以修改发生错误的就可以了
就如上述的例子,如果滴滴司机中途跟你说:实在不好意思,不能接单,那么就可以重新打
4.类和对象
在现实世界中,属于同一类的对象很多,类是抽象的,不是具体的,我们人习惯以对象的方式认识现实世界;
例如:学生这个类。如果单说这个学生的话,会觉得抽象,这个学生的学号呢?姓名呢?性别呢?等等
那么 这个学生就是一个模板(类),通过这个模板我们可以创建很多实体化的东西(实例化对象)。
比如:我创建了一个学生的模板(类) 学号:001,姓名:张三,性别:男
通过上述的例子就可以认识到 类和对象的关系了, 我们通过模板来创建一个实例。也就是通过类创建对象
tips:类是抽象的,对象是具体的,对象是类的实例化;
类是一组相关属性和行为的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物。
- 属性:该事物的状态信息;
- 行为:该事物的功能信息;
第八章创建对象和使用
1.Java类的定义
public class 类名 {
//成员变量
//成员方法
}
- 定义类: 就是定义类的成员,包括成员变量和成员方法。
- 成员变量: 和以前定义变量几乎是一样的。只不过位置发生了改变。在类中,方法外。
- 成员方法: 和以前定义方法几乎是一样的。只不过把static去掉,static的作用在面向对象后面再讲解。
类的定义格式举例:
/*
注意:不需要写main方法,Student只是一个模板而已
*/
public class Student {
//成员变量(属性) 描述事物的信息
String name; //姓名
int age; //年龄
// 成员方法(行为) 描述事物的行为
public void study(){
System.out.println("学习");
}
public void eat(){
System.out.println("吃饭");
}
}
2.类的实例化(创建对象)
类是抽象的,不是具体的,类只是负责把事物描述起来,提供模板;对象是类的实例化,是具体的;我们定义好一个Java类后需要通过对象将类进行实例化;
创建对象的语法:new
类名 对象名=new 类名()
使用对象访问类中的成员:引用.
的方式(这里又称对象名为引用,意思就是只想堆内存的指针)
对象名.成员变量;
对象名.成员方法();
类的实例化练习:
public class Demo {
public static void main(String[] args) {
//创建对象格式: 类名 对象名 = new 类名();
Student s = new Student();
System.out.println("s: " + s); //s: com.song.Student@1540e19d 这里是为什么后面解释
//直接输出成员变量的值
System.out.println("姓名: " + s.name); //null
System.out.println("年龄" + s.age); //0
System.out.println("--------");
//给成员变量赋值
s.name = "张三";
s.age = 21;
//再次输出成员变量的值
System.out.println("姓名: " + s.name); //张三
System.out.println("年龄: " + s.age); //21
System.out.println("---------");
//调用成员方法
s.study(); //学习
s.eat(); //吃饭
}
}
3.成员变量的默认值
在上方的案例当中,我们直接输出了成员变量的值 name(String类型) 为null ,age(int类型)为0
那么 就有了成员变量的默认值
基本类型:
数据类型 | 默认值 |
---|---|
整数(byte,short,int,long) | 0 |
浮点数(float,double) | 0.0 |
字符(char) | ‘\u0000’ |
布尔(boolean) | false |
引用数据类型
数据类型 | 默认值 |
---|---|
数组,对象,String | null |
4.JVM内存
在上面"方法"一章说到了JVM有三大内存空间,只说到了方法区和栈区。接下载就说说堆内存
- 方法区:类加载器classloader,将硬盘上的XXX.class字节文件码装载到jvm的时候,会将字节码文件放到方法区中。也就是方法区中存储的是代码片段,因为类需要加载,所以方法区中最先有数据
- 栈:在方法被调用的时候,该方法需要的内存空间在栈中分配。
栈式使用最频繁的,一直压栈和弹栈
方法调用的时候压栈,栈中最主要的存储时局部变量,局部变量时在方法当中的。
就比如,我们先执行main方法,压入栈中,我们称main方法栈帧 - 堆内存:凡是用过,new运算符创建的对象,都在堆内存当中。new运算符的作用就是在堆内存中开辟一块空间。堆中存储“对象”,以及对象的实例变量
上图!
5.创建对象练习以及JVM内存图
Student类:
public class Student {
//属性(描述状态),在java程序中以“成员变量”的形式存在
//学号
int no;//这种变量又称为实例变量,一个对象一份
//姓名
String name;
//年龄
int age;
//性别
boolean sex;
//住址
String addr;
}
类的实例化:
public class StudentTest02 {
public static void main(String[] args) {
/*
访问学生姓名可以直接通过类名吗
学生姓名是一个实例变量,实例变量是对象级别的变量
是不是应该先有对象才能说姓名的事情
不能通过”类名“来直接访问”实例变量“
System.out.println(Student.name);
*/
//应该注意,现有类才能创建对象,而有了对象,我们才能去对变量进行操作
//创建对象一
//s1这个局部变量叫做引用
Student02 s1=new Student02();
//创建对象二
//s2这个局部变量也叫引用
//怎么访问实例变量
//语法: 引用(通过new出来的变量).实例变量名
System.out.println(s1.no);
System.out.println(s1.name);
System.out.println(s1.sex);
System.out.println(s1.age);
System.out.println(s1.addr);
System.out.println("-------------------------");
Student02 s2=new Student02();
//这里的s1 s2都是属于局部变量
System.out.println(s2.no);
System.out.println(s2.name);
System.out.println(s2.sex);
System.out.println(s2.age);
System.out.println(s2.addr);
System.out.println("-------------------------");
//代码到此处我可以修改学生s1的学生属性吗
//通过”=“赋值的方式将内存中实例变量的值修改一下
s1.no=110;
s1.name="张三";
s1.age=20;
s1.sex=true;
s1.addr="唐山学院";
System.out.println(s1.no);
System.out.println(s1.name);
System.out.println(s1.sex);
System.out.println(s1.age);
System.out.println(s1.addr);
}
}
JVM内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TgqyG5x8-1670474925478)(https://foruda.gitee.com/images/1664349585560247395/4755859a_10637153.png)]
6.成员变量和局部变量
根据声明的位置不同,变量也有不同的名称,当然存放的位置也可能不同,下面进行代码测试
public class Student {
int age;//成员变量 年龄
String name;//成员变量 姓名
//成员方法 学习方法
public void study(){
int i = 10; //局部变量
System.out.println("学生正在学习");
}
/**
* 自我介绍的方法
* @param name 姓名(局部变量)
* @param age 年龄(局部变量)
*/
public void intro(String name,int age){
System.out.println("大家好我叫"+name+"我今年"+age+"岁了");
}
}
成员变量和局部变量的区别:
- 成员变量:
- 成员变量定义在类中,在整个类中都可以被访问。
- 成员变量分为类成员变量和实例成员变量,实例变量存在于对象所在的堆内存中
- 成员变量有默认初始化值。
- 成员变量的权限修饰符可以根据需要,选择任意一个
- 局部变量:
- 局部变量只定义在局部范围内,如:方法内,代码块内等。
- 局部变量存在于栈内存中,当方法弹栈(执行完毕)后,局部变量销毁;
- 局部变量没有默认初始化值,使用前必须手动赋值;
- 局部变量声明时不指定权限修饰符;
就现在而言总体来说:在方法内部的就是局部变量存放的位置在栈区, 在方法外部的就是成员变量 存放的位置为堆区或者是方法区(这个到后面将static会解释)
7.方法调用传参问题
类:
class ren{
int age;
}
测试类:
/*
java中关于方法调用时参数传递实际上只有一个规则
不管你是什么类型的数据,实际上传递的时候,都是将变量中保存的那个值复制一份传过去
*/
public class Test {
public static void main(String[] args){
ren r=new ren();
r.age=10;
add(p);
/*
这里是将一个对象的内存地址传到了add中,
也就是add中的对象和main中的对象 保存的地址都指向了堆内存同一个类型的对象
但是我们要注意 main中的p 和add中的p 是两个p
*/
System.out.println("main--->"+p.age);//11
}
//方法中的参数可以是基本的也可以是引用的
public static void add(ren r){
//这里会让堆内存中age这个属性下存放的值发生改变
p.age++;
//理解为c语言的指针,形参和实现指向同一内存地址
System.out.println("add--->"+p.age);//11
}
}
/*
最后输出为
add--->11
main--->11
*/
在此需要明确什么是值传递和址传递
- 值传递:(参数类型是基本数据类型):方法调用时,实参把它的值传递给对应的形参,形参只是用实参的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形参值的改变不影响实参的值。
- 引用传递:(参数类型是引用数据类型参数):也称为传地址。方法调用时,实参是对象(或数组),这时实参与形参指向同一个地址,在方法执行中,对形参的操作实际上就是对实参的操作,这个结果在方法结束后被保留了下来,所以方法执行中形参的改变将会影响实参。
在Java中,除了基本数据类型之外的数据类型都是引用数据类型,都是通过new在堆内存开辟空间;
对上方的代码,内存图解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6tvjxWiC-1670474925481)(https://foruda.gitee.com/images/1664360660659307046/855491e0_10637153.png)]
8.this关键字
-
this是一个关键字,是一个引用,保存内存地址指向自身
-
this可以使用在实例方法中,也可以使用在构造方法中
-
this出现在构造方法中其实代表的是当前对象
-
this不能使用在静态方法中
-
this大部分情况下是可以省略的,在区分局部变量和实例变量的时候不能省略
-
this()这种语法只能出现在构造方法中的第一行,表示当前构造方法调用本类其他的构造方法
public class ThisTest(){ int year; int month; int day; public date(){ this(1970,1,1); } public date(int year,int month,int day){ this.year=year; this.month=month; this.day=day; } public static void main(String[] a){ date d1=new date();//1970,1,1 date d2=new date(1999,2,1);//1999,2,1 } }
this内存结构图
通过图解可以看出,this的中保存的内存地址和引用是一样的
9.static关键字
9.1static概述
static
关键字它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属于某个对象的。被static修饰的成员由该类的所有实例(对象)共享;
static修饰的统一都是静态的,都是类相关的,不需要new对象,之间采用”类名.“访问当一个属性是类级别的属性,所有对象的这个属性的值都是一样的,建议定义为静态变量
9.2定义和使用格式
9.2.1类变量
当 static
修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作,因为该变量属于类,而不是某个对象。
类变量: 使用 static关键字修饰的成员变量。
定义格式:
static 数据类型 变量名;
使用格式:
类名.变量名
例如:
static String name;
如果一个类当中,每个对象都有公共的字段呢?每次new都会实例化一个新的公共字段,那显然是浪费内存的,所以有没有一种方法可以把公共的属性提出来.就是static关键字(到下面会讲解静态原理图)
public class StaticTest {
public static void main(String[] args) {
//访问中国人的国际
//静态变量应该使用"类名."的方式访问
System.out.println(Chinese.guoji);
Chinese c1=new Chinese("123","张三");
System.out.println(c1.name);
System.out.println(c1.zheng);
Chinese c2=new Chinese("456","李四");
System.out.println(c2.name);
System.out.println(c2.zheng);
}
}
class Chinese{
//身份证号
//每个人的身份证号不同,所以身份证号都应该是实例变量,一个对象一个
String zheng;
//名字也是一个人一个
String name;
//对于zhongguo这个类来说,国际都是中国,不会随对象的改变而改变。
//国籍属于整个类的特征
//加static的变量叫静态变量
//静态变量在类加载时初始化,不需要new对象,静态变量的空间就开出来了
//静态变量存储在方法区
static String guoji="中国";
//无参数
public Chinese(){
}
//有参数
public Chinese(String zheng,String name){
this.zheng=zheng;
this.name=name;
}
}
9.2.2 静态方法
当 static
修饰成员方法时,该方法称为类方法。静态方法在声明中有 static ,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。
类方法:使用 static关键字修饰的成员方法,习惯称为静态方法。
定义格式:
修饰符 static 返回值类型 方法名 (参数列表) {
// 执行语句
}
举例:在Student类中定义静态方法
public static void showId(int id) {
System.out.println("id:" + id);
}
静态方法调用的注意事项:
- 静态方法可以直接访问类变量(被static修饰的变量)和静态方法。(静态方法能够访问静态资源)
- 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
- 静态方法中,不能使用this关键字
tips:静态方法只能访问静态成员。
9.2.3调用格式
// 访问类变量
类名.类变量名;
// 调用静态方法
类名.静态方法名(参数);
总结:
成员变量和成员方法的访问:
引用.
的形式
- 引用.成员变量
- 引用.成员方法
这里要注意,要先有对象(先new对象)才能去使用
引用.
的形式否则是无法访问静态变量和静态方法
- 静态变量:类名.类变量名
- 静态方法:类名.静态方法名(参数)
9.3静态原理图解
static
修饰的内容:
- 是随着类的加载而加载的,且只加载一次。
- 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
- 它优先于对象存在,所以,可以被所有对象共享。
在JDK1.8以前,静态成员存储在方法区(永久代)中,此时方法区的实现叫做永久代
在JDK1.8以后,永久代被移除,此时方法区的实现更改为元空间,但由于元空间主要用于存储字节码文件,因此静态成员的存储位置从方法区更改到了堆内存中
9.4静态代码块
-
使用static关键字可以定义:静态代码块
-
定义格式
static{ java语句 }
-
static静态代码块在什么时候执行的呢?
- 类加载时执行,并且只执行一次,静态代码块由这样的特征
-
静态代码块在类加载时执行,并且在main方法之前执行
-
静态代码块是自上而下顺序执行
-
静态代码块的作用
- 第一:静态代码块不这么常用(不是每个类都要写的东西)
- 第二:静态代码块这种语法机制实际上是sun公司给我们java程序员的一个特殊的实际这个时机叫类加载时机
9.5实例代码块
执行的时机:只有时构造方法,必然在构造方法之前,执行一次
语法:
{
java语句;
}
9.6 Static的应用-单例设计模式
- 单例是一种设计模式,是为了解决某个问题
- 单例能解决的问题:保证一个类对外只能产生一个对象。
重点在于如何实现单例:
-
饿汉单例
-
特点:拿对象时,对象已经存在
-
实现步骤:
/** * 饿汉单例设计模式 */ public class SingleInstance { // 1.定义一个类,将构造器私有 private SingleInstance() {} // 2.对外提供一个静态对象 // 饿汉单例是在获取对象前,就已经准备好了一个对象 // 这个对象只能通过类名访问,所以定义成static的 public static SingleInstance instance = new SingleInstance(); }
-
-
懒汉单例
-
特点:要拿对象的时候,才开始创建一个对象。
-
实现步骤:
/** * 懒汉单例设计模式 */ public class SingleInstance2 { // 2.定义一个静态的成员变量负责存储一个对象,只加载一次,只有一份。 // 专业的做法是:这里私有化,这样可以避免给别人挖坑 private static SingleInstance2 instance2; // 1.将构造器私有化 private SingleInstance2() {} // 3.对外提供一个静态的获取对象的方法 public static SingleInstance2 getInstance2() { if (instance2 == null) { // 第一次来拿对象 :此时需要创建对象。 instance2 = new SingleInstance2(); } return instance2; } }
-
10.构造方法
10.1概念
什么是构造方法呢?
- 构造方法是一个特殊的方法,通过构造方法可以完成对象的创建,以及实例化变量的初始化。换句话说:构造方法是用来创建对象,并且同时给对象的属性赋值
注意:实例变量没有手动赋值的时候,系统会赋默认值 - 也就是当我们new 的时候 就会调用构造方法
在类当中没有写构造方法为什么还能调用呢?
- 当一个类没有提供任何构造方法时,系统会默认提供一个无参数的构造方法(而这个构造方法叫做缺省构造器)
构造方法的语法?
-
[修饰符列表] 构造方法名 (形式参数列表){ 构造方法体; }
-
注意:
- 第一:修饰符列表统一写public 不要写成public static
- 第二:构造方法名必须和类名一样
- 第三:构造方法不需要指定返回值类型,也不能写void,写上以后就是普通方法了
构造方法的特点
- 构造方法是支持方法重载
- 在一个类中构造方法可以有多个,并且所有的构造方法名字都是一样的
- 对于实例变量来说,只要你在构造方法中没有手动给他赋值,统一都会默认赋值,默认赋值系统值
实例变量是什么时候完成初始化的呢?
实例变量是在构造方法执行的过程中完成初始化的
9.2代码测试
vip类:
/*
1.构造方法可以有多个.
2.如果我们没有写构造方法,那么系统会默认提供一个无参数的构造方法(缺省构造器)
3.构造方法支持方法的重载,可以写一个参数的构造方法,可以写连两个参数的构造方法.也可以写全参的构造方法
4.如果我们写了构造方法,那么无参的构造方法就不在提供,所以为了保险起见 我们要把无参数的构造方法手动写上
*/
public class Vip {
long no;
String name;
String birth;
boolean sex;
public Vip(){
}
public Vip(long no,String name){
//给实例变量赋初值【初始化实例变量,初始化属性】
this.no=no;
this.name=name;
//这里实际上还有两行代码
//this.birth=null
//this.sex=false
}
public Vip(long no,String name,String birth){
no=no;
this.name=name;
this.birth=birth;
//这里实际上还有一行代码
//this.sex=false
}
public Vip(long no,String name,String birth,boolean sex){
this.no=no;
this.name=name;
this.birth=birth;
this.sex=sex;
}
}
代码测试:
public class ConstructorVip {
public static void main(String[] args) {
//无参构造方法
Vip u1=new Vip();
System.out.println(u1.no);
System.out.println(u1.name);
System.out.println(u1.birth);
System.out.println(u1.sex);
//有参构造方法
Vip u4=new Vip(3333L,"黑寡妇","2001-2-03",false);
System.out.println(u4.no);
System.out.println(u4.name);
System.out.println(u4.birth);
System.out.println(u4.sex);
}
}
11.package和import
-
为什么要用package
- package是java中的包机制。包机制的作用是为了方便程序的管理
- 不同功能的类分别存放在不同的包下。(按照功能划分的,不同的软件包具有不同的功能)
-
package怎么用?
- package是一个关键字,后面加包名
- 注意:package语句只运行出现在java源代码的第一行
-
包名命名规范
- 一般采用公司域名倒叙的方式(因为公司域名具有全球唯一性)
- 公司域名倒叙名+项目名+模块名+功能名
-
编译带有包的java程序
-
编译
javac -d . Ptest.java
-
运行
java packagetest.Ptest01// packagetest.Ptest这个是类名
-
12.访问权限
访问权限有那些呢?
- private私有的
- protected受保护的
- public公开的
- 默认的(什么也不写)
访问控制修饰符 | 本类访问 | 同包访问 | 子类访问 | 编写位置任意 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 可以 | 不行 |
默认 | 可以 | 可以 | 不行 | 不行 |
private | 可以 | 不行 | 不行 | 不行 |
访问控制权限可以修饰什么?
- 属性(四个都可以)
- 方法(四个都可以)
- 类(public和默认可以)
- 接口(public和默认可以)
第九章面向对象的三大特征
1.封装
1.1封装概述
封装是面向对象的三大特征之一,面向对象编程语言是对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界无法直接操作和修改。封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。要访问该类的数据,必须通过指定的方式。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性
举例:
public class Person {
int age;
}
这里我们先不封装来分析一下程序的缺点:
- 如果不去封装,那么这个属性就是对外暴露的,随便去访问都可以.这显然是不安全的。
举个例子 如果我们给Person的age属性赋值为300呢?
那显然是不合理的。人的年龄怎么会超过300岁呢?
所以我们要对age属性进行封装。加强安全性!
1.2private关键字
- private是一个权限修饰符,代表最小权限。
- 可以修饰成员变量和成员方法。
- 被private修饰后的成员变量和成员方法,只在本类中才能访问。
封装的原则:将属性隐藏起来,若需要访问某个属性,提供公共方法对其访问(setter和getter方法)
使用private
修饰成员变量,代码如下:
public class Person {
//年龄
//private表示私有的,被这个关键字修饰之后,该数据只能在本类中访问
//出了这个类,age属性就无法访问了
private int age;
//封装的第二步:对外提供的公开的set(写) get(读)方法作为操作入口(并且都不带static,都是实例方法)
/*
注意:
在java开发中 get方法和set方法应该满足一下格式
public 返回值类型 get+属性名首字母大写(无参){}
public void set+首字母大写(有一个参数){xxx=参数;}
*/
public int getAge(){
return age;
}
public void setAge(int age){
//能不能在这里设置关卡
if (nianling>150 ||nianling<0) {
System.out.println("对不起,您输入的年龄有问题");
return;
}
//程序到了这里说明,年龄合法的了
this.age = age;
}
}
总结:
1.封装
第一点要记住:有个封装才能有继承,有了继承才能有多态
2.面向对象得首要特征:封装
作用:保证内部得安全性,屏蔽复杂,暴露简单
3.在代码级别上,封装有什么用?
一个类体当中得数据,假设封装之后,对于代码得调用人员来说,不需要关心代码得复杂实现,只需要通过一个简单得入口就可以访问了。
另外,类体中安全级别较高得数据封装起来,外部人员不能随意访问,来保证数据得安全性
4.封装的代码实现两步
第一步:属性的私有化
第二步:一个属性对外提供set和get方法。外部程序只能通过set方法修改,只能通过get方法读取,可以在set方法中设立关卡来保证数据的安全性
在强调一下:set和get方法都是实例方法,不能带有static
不带static的方法称为实例方法,实例方法的调用必须现有new对象
set方法:
public void set+属性名首字母大写(一个参数){xxx=一个参数}
get方法:
public 返回值类型 get+属性名首字母大写(无参){return xxx; }
1.3 工具类的定义
工具类是什么?
- 工具类中都是静态方法,为了完成一个共用功能
好处:一次编写,处处可调,提高代码的复用性,提高开发的效率。
注意:建议把工具类的构造器私有化,因为工具类无需对外创建对象,它的方法都是静态方法,直接用类名访问即可。
public class CodeUtil {
// 验证码工具类
// 静态成员方法
// 由于工具类中的方法都是静态方法,直接用类名调用即可,
// 所以,建议将工具类的构造器使用private修饰
private CodeUtil() {
}
public static String createCode(int n) {
String code = "";
String data = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random r = new Random();
for (int i = 0; i < n; i++) {
int index = r.nextInt(data.length());
code += data.charAt(index);
}
return code;
}
}
2.继承
2.1继承概述
- 子类继承父类可以获得父类的功能,提高代码的复用性
- 子类可以重写(覆盖)某些父类的功能,我们一般称为增强
- 子类除了可以继承父类的功能之外,还可以额外添加子类独有的功能。
- 继承的作用:
- 基本作用:子类继承父类,代码可以得到复用
- 主要作用:因为有了继承关系,才有后期的方法的覆盖和多态机制
那么在实际开发中,满足什么条件才使用继承呢?
凡是采用“is a”能描述的,都可以去继承
例如:cat is a Animal 猫是一个动物,dog is Animal 狗是一个动物
假设以后的开发中有一个A类,有一个B类,A类和B类确实有重复的代码,那么他们两个之间可以继承吗?
不一定,还要看他们之间是否可以用is a去描述
2.2继承语法
class 父类
class 子类 extends 父类
注意点:
- B类继承A类,则称A类为超类(superclass)、父类、基类,B类称为子类(subclass)、派生类、扩展类
- java中的继承只支持单继承,不支持多继承
- 虽然java中不支持多继承,但有的时候会产生间接继承的效果
2.3案例
举例:
- 老师
- 属性(成员变量):姓名,年龄,性别
- 行为(成员方法):吃饭,睡觉,授课
- 学生
- 属性(成员变量):姓名,年龄,性别
- 行为(成员方法):吃饭,睡觉,听课
1)分析一下这两个对象有什么公共的部分,姓名、年龄、性别、吃饭、睡觉 都是公共的部分。只有授课和听课不同
定义父类,提取公共的部分
//提取公共部分,封装到Person类中
public class Person{
String name;//姓名
int age;//年龄
boolean sex;//性别
//吃饭的行为
public void eat(){
System.out.println(name+"正在吃饭");
}
public void sleep(){
System.out.println(name+"正在睡觉");
}
}
2)定义一个老师的类,继承Person类,也就是将父类的属性和行为都继承过来.自己扩展一个授课的行为
public class Teacher extends Person{
public void teach(){
System.out.println(name+"授课");
}
}
3)定义一个老师的类,继承Person类,也就是将父类的属性和行为都继承过来.自己扩展一个上课的行为
public class Student extends Person{
public void study(){
System.out.println(name+"学习");
}
}
4)测试类
public class Test{
public static void main(String[] args){
//创建教师对象
Teacher t = new Teahcer();
t.name="张三";
t.age=38;
t.sex=true;//true表示男人
t.teach();
t.eat();
t.sleep();
//创建学生对象
Student t = new Student();
t.name="李四";
t.age=21;
t.sex=false;//true表示男人
t.study();
t.eat();
t.sleep();
}
}
2.4父类不可继承的内容
父类中所有的内容,不是都可以让子类继承的,以下两个内容就不可以让子类继承
- 被private修饰的(这里访问要通过setter和getter方法)
- 构造方法不能被继承
2.5成员变量的继承
当继承父类之后,成员变量的方法名重名和不重名是否有冲突呢?
2.5.1成员变量不重名
子类和父类不重名的情况下,直接继承过来没有任何的影响
public class Demo {
public static void main(String[] args) {
Zi zi=new Zi();
System.out.println("父类num:"+zi.num1);//父类继承下来的
System.out.println("子类num:"+zi.num2);//这个是本身的
}
}
class Fu{
int num1=10;
}
class Zi extends Fu{
int num2=20;
}
结果输出
父类num:10
子类num:20
2.5.2成员变量重名
当成员变量重名的时候呢?
public class Test {
public static void main(String[] args) {
Zi zi=new Zi();
System.out.println(zi.num);
System.out.println(zi.num);
}
}
class Fu{
int num=10;
}
class Zi extends Fu{
int num=20;
}
测试结果
20
20
经过测试可以得出,当子类和父类的成员变量名重名的时候,就会采用就近原则使用子类的成员变量
2.6成员方法的继承(重写)
2.6.1什么是方法的重写
如果子类和父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法;
但是如果是重名的时候呢?
和成员变量的继承是一样的,当成员方法重名的时候会调用子类的方法,这么机制叫做方法的重写(Override)
在此回顾以下方法的重载:
当一个类中,如果功能相似的话,建议将名字定义的一样,这样代码美观
条件一:在同一个类中
条件二:方法名相同
条件三:参数列表不同(个数、顺序、类型)
2.6.2什么时候采用重写机制
子类继承父类之后,放继承过来的方法无法当前子类的业务需求时,子类有权力对这个方法进行重新编写.则有必要进行方法的覆盖(重写),
在前面就提到,成员方法描述的是,对象的行为。当子类和父类行为有一些差异的时候,但都属于一种同一种行为,只是行为方式不一样.就比如父类采用的学习方式是线下课(行为),而子类采用学习的方式是网课。显然这两种行为都是学习,但只是方式不同,所以我们要进行方法的重写。
举例 :没有使用方法的重写
public class OverrideTest {
public static void main(String[] args) {
//创建鸟儿对象
Bird b1=new Bird();
//让鸟儿移动
b1.move();//动物在移动
}
}
//父类
class Animal{
public void move(){
System.out.println("动物在移动");
}
}
//子类
class Bird extends Animal{
//子类继承父类中,有一些“行为”可能不需要改进,有一些“行为”可能面临这必须改进
//因为父类中继承过来的方法已经无法满足子类的需求
//鸟儿在移动的时候希望输出鸟儿在飞翔
}
class Cat extends Animal{
//猫在移动的时候,我希望输出:猫在走猫步
}
上方案例的改进
public class OverrideTest {
public static void main(String[] args) {
bird b=new bird();//鸟儿在飞行
b.move();
Dog c=new Dog();//狗在走狗步
c.move();
}
}
class animal{
public void move(){
System.out.println("动物在移动");
}
}
class bird extends animal{
//对move方法进行方法的覆盖,方法的重写
//最好将父类中的方法原封不动的复制过来
//方法覆盖,就是将继承过来的那个方法覆盖掉了,继承过来的方法没了
public void move(){
System.out.println("鸟儿在飞行");
}
}
class Dog extends animal{
public void move(){
System.out.println("狗在走狗步");
}
}
结论:
当子类对父类继承过来的方法进行”方法覆盖“之后,
子类对象调用该方法的时候,一定执行覆盖之后的方法
2.6.2重写的注意点
- 条件一:两个类必须要有继承关系
- 条件二:重写之后的方法和之前的方法具有:
相同的返回值类型
相同的方法名
相同的形式参数列表 - 条件三:访问权限不能更低,可以更高(子类重写的方法,访问权限不能更低只能更改)关于访问权限后面会有讲解
- 条件四:重写之后的方法不能比之前的方法抛出更大的异常,可以更少(异常会到后期有讲解)
这里还有几个注意事项:
注意一:方法覆盖只是针对方法,和属性无关
注意二:私有方法无法覆盖
注意三:构造方法不能被继承,所有构造方法也不能被覆盖
注意四:方法覆盖只是针对于实例方法,静态方法覆盖没有意义
2.6.3方法重载和方法覆盖有什么区别?
- 方法重载发生在同一个类当中。
- 方法覆盖是发生在具有继承关系的父子类之间。
- 方法重载是一个类中,方法名相同,参数列表不同。
- 方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致:
方法名一致、参数列表一致、返回值类型一致。
2.7super关键字
写super关键字之前,思考两个问题
- 当子类的成员变量名称和父类的成员变量名称重名的时候,那么我们怎么访问父类的成员变量呢?
- 当子类的成员方法名称和父类的成员方法名称重名的时候,那么我们怎么访问父类的成员方法呢?
这个时候就体现了super的重要性了
super
关键字:用于修饰父类成员变量,类似于之前学过的 this
;this
代表的是本类对象,而super
代表的是父类对象;使用super
我们可以调用父类的成员(属性和行为),注意super关键字不能访问父类私有(private修饰)的成员
子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量(不是private修饰的)时,需要使用 super
关键字,修饰父类成员变量,类似于之前学过的 this
。
super关键字的作用:
- 用于访问父类中定义的属性
- 用于调用父类中定义的成员方法
- 用于在子类构造方法中调用父类的构造器
2.7.1.this和super的图解
先上示例
public class SuperTest {
public static void main(String[] args) {
Zi zi = new Zi();
System.out.println(zi.num);
zi.method();
}
}
class Fu{
private int num=20;
public void method(){
System.out.println("父类的method");
}
}
class Zi extends Fu{
int num=10;
public void method(){
System.out.println("父类的method");
}
}
- main方法进栈执行;
- 执行new Zi(),首先现将Fu、Zi两个类加载到内存(方法区)
- 初始化Fu类,再初始化子类;子类中保留父类的引用super;可以通过super来获取父类的非私有内容;
- 子类调用method方法,调用的是this区中的方法,因此输出的是Zi method…
2.7.2super()
super,是手动调用还是自动的去调用呢?
示例:
public class SuperTest {
public static void main(String[] args) {
Zi zi1=new Zi();
Zi zi2=new Zi(20);
}
}
class Fu{
String name;
int age;
public Fu() {
System.out.println("Fu类的无参数构造方法执行了");
}
public Fu(String name, int age) {
System.out.println("Fu类的全参构造方法执行了");
this.name = name;
this.age = age;
}
}
class Zi extends Fu{
int id;
public Zi(){
System.out.println("Zi类的无参数构造方法执行了");
}
public Zi(int id){
System.out.println("Zi类的有参数构造方法执行了");
this.id=id;
}
}
测试内容
Fu类的无参数构造方法执行了
Zi类的无参数构造方法执行了
Fu类的无参数构造方法执行了
Zi类的有参数构造方法执行了
得出结论:
-
在初始化子类的时候(new),会先调用父类的无参数构造方法,来实例化父类(也就是先有父后有子)
-
所以在父类当中,一定要手动写上无参数的构造方法.这样才能不会出错,确保父类一定会实例化
-
我们没有手动写上super这个关键字,也会调用父类的无参数构造方法。那会是什么位置呢?
-
在测试结果上我们可以看出,"Fu类的无参数构造方法执行了"总是比"Zi类的无参数构造方法执行了"先执行,那么系统一定会在构造方法的首行写调用super这个关键字的,由于是调用的构造方法
所以在首行会默认的有一个super()
(this()
是调用本类的构造方法,super()
是调用父类的构造方法)class Zi extends Fu{ int id; public Zi(){ super(); System.out.println("Zi类的无参数构造方法执行了"); } }
注意:父类的无参数构造方法一定要手动写出来
注意:在前面说到this()也是出现在构造方法的首行的,所以this()和super()不能共存
2.7.3.访问父类
在前面说到用private
修饰的无法去访问,那么既然学了super 就可以调用父类的setter和getter方法去访问
如果成员变量和成员方法和父类的一样那么怎么去访问呢?
示例:
class Fu{
private String name;
public Fu() {
}
public Fu(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void method(){
System.out.println("父类的方法");
}
}
class Zi extends Fu{
public void method() {
System.out.println("子类的方法");
//System.out.println(super.name);父类私有成员不可以通过super获取
System.out.println(super.getName());// 获取父类的方法
super.method();//执行父类的方法
}
}
2.7.4.this和super对比的去学
this关键字:
- this能出现在实例方法中和构造方法中
- this的语法是:”this.“ ”this()“
- this不能使用在静态方法中。
- this. 大部分是可以省略的
- this. 在区分句部变量和实例变量时候不能省略
- this()只能出现在构造方法第一行中,通过当前的构造方法去调用”本类“中
其他的构造方法,目的是:代码复用
super关键字
- super能出现在实例方法中和构造方法中
- super的语法是:”super.“ ”super()“
- super不能使用在静态方法中。
- super. 大部分是可以省略的
- super. 在什么时候不能省略呢??
父类和子类中有同名属性,或者说有同样的方法,
想在子类中访问父类的 super.不能省略 - super()只能出现在构造方法第一行中,通过当前的构造方法去调用”父类“中
的构造方法,目的是:创建子类对象的时候,先初始化父类特征。
3.多态
3.1多态概述
面向对象的三大特性——封装、继承、多态;前两个我们都学习过了,多态也是面向对象中一项重大的特征,多态体现了程序的可扩展性、代码的复用性等;
多态:即事物(对象)存在的多种形态,简称多态;
在生活中,多态处处看见;如"我要吃水果",这个水果有很多种形态,苹果、梨子、香蕉等都是水果;再比如说话,猫的叫声是喵喵,狗的叫声是汪汪,苍蝇的叫声蝇蝇等;同一行为,通过不同的事物,可以体现出来的不同的形态;
3.2多态案例
现有一家宠物店:宠物店有寄存宠物的功能,但是运营之初,只能寄存狗,针对狗这个类,有吃饭、睡觉的行为;
多态的用法,就是基于这个案例来实现的
3.2.1不使用多态的写法
1)定义狗类:
public class Dog {
public void eat(){
System.out.println("狗狗正在吃饭");
}
public void sleep(){
System.out.println("狗狗正在睡觉");
}
}
- 定义一个宠物店
public class PetShop {
//存放狗
public void DogShop(Dog dog){
dog.eat();
dog.sleep();
}
}
但是运营一段时间以后,宠物店强大了起来,就可以寄存猫了
1)定义猫类
public class Cat {
public void eat(){
System.out.println("小猫正在吃饭");
}
public void sleep(){
System.out.println("小猫正在睡觉");
}
}
2)扩展原来PetShop宠物店类
public class PetShop {
//存放狗
public void DogShop(Dog dog){
dog.eat();
dog.sleep();
}
//存放猫
public void DogShop(Cat cat){
cat.eat();
cat.sleep();
}
}
到这里需要注意了,如果宠物店又强大可以寄存鹦鹉呢?那是不是还要在之前的宠物店这个类继续扩展,如果扩展一个,两个觉得没啥,但是要扩展上百个上千个呢。还总是要修改一些重复的代码
3.2.2多态的写法
1)定义所有宠物的父类
public class Animal {
public void eat(){
System.out.println("动物正在吃饭");
}
public void sleep(){
System.out.println("动物正在睡觉");
}
}
2)修改上方的狗类去继承宠物类
public class Dog extends Animal {
public void eat(){
System.out.println("狗狗正在吃饭");
}
public void sleep(){
System.out.println("狗狗正在睡觉");
}
}
3)修改上方的猫类去继承宠物类
public class Cat extends Animal{
public void eat(){
System.out.println("猫猫正在吃饭");
}
public void sleep(){
System.out.println("猫猫正在睡觉");
}
}
4)修改上方的宠物店类
public class PetShop {
//存放所有的动物
public void AnimalShop(Animal animal){// Animal animal=new Dog();
animal.eat();
animal.sleep();
}
}
可能在这里还是不知道 这哪里是多态了呢?最后寄存宠物的时候,存放的是所有宠物的父类而不是存放的各个宠物啊。
下面进行一个测试代码:
public class Test {
public static void main(String[] args) {
//先创建一个宠物店类
PetShop petShop=new PetShop();
//创建狗狗类
Dog dog=new Dog();
//寄存动物
petShop.AnimalShop(dog);
}
}
测试结果:
狗狗正在吃饭
狗狗正在睡觉
根据测试结果,在宠物店寄存方法中,参数是所有宠物的父类
public void AnimalShop(Animal animal){
animal.eat();
animal.sleep();
}
那为什么输出的狗狗在吃饭和睡觉呢?
我们new的是狗狗的对象,是宠物类的子类,在寄存方法中接收到的参数为
Animal animal=new Dog();
这就引出了多态的用法,向上转型和向下转型 下面详细讲解
3.3多态用法
多态的前提
- 继承或者实现(实现到后面来讲)
- 方法的重写(子类方法的重写)
- 父类引用指向子类对象
一句话概括多态就是父类引用指向子类对象,在上一章案例中就是父类引用(Animal)指向了子类对象(Dog、Cat),多态一般伴随着重写的出现,调用者是父类,具体执行的方法则是子类(子类重写了父类的方法运行的是子类),因为只有子类的功能才能凸显多态性,不同的子类具备的功能是不一样的,那么有重写肯定就会有继承或者实现;
多态的格式
父类类型 变量名 = new 子类对象;
变量名.方法名();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写方法。
3.4多态的转型
转型分为:
- 向上转型:自动类型提升;多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。子类型重写的方法
子----->父(自动类型转化)----> 父类 引用名 = new 子类(); - 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。子类型特有的方法
父----->子(强制类型转换,需要加强制类型转换符) 子类 引用名 = (子类)父类引用;
tips:
向上转型和向下转型可以理解为自动类型转换和强制类型转换
自动类型转换:小范围(子类)—>大范围(父类)
强制类型转换:大范围(父类)—>小范围(子类)
3.4.1向上类型转换
子类重写父类的方法,在调用方法的时候 会指向子类的方法
示例:水果摊
-
定义水果的父类
public class Fruit { public void buy(){ System.out.println("客户买了水果"); } }
-
定义苹果类
public class Apple extends Fruit{ public void buy() { System.out.println("客户买了苹果"); } }
-
定义香蕉类
public class Banana extends Fruit{ public void buy() { System.out.println("客户买了香蕉"); } }
-
定义客户类
public class Customer { //这里Fruit fruit=new Apple() //Fruit fruit=new Banana() public void buyFruit(Fruit fruit){ fruit.buy(); } }
-
测试类
public class Test { public static void main(String[] args) { //创建用户类 Customer customer=new Customer(); //这个就是两行代码的合并 Apple apple =new Apple(); customer.buyFruit(apple); //用户购买苹果 customer.buyFruit(new Apple()); //用户购买香蕉 customer.buyFruit(new Banana()); } }
-
测试结果
客户买了苹果 客户买了香蕉
在客户类当中
fruit.buy();
java程序分为编译阶段和运行阶段
先来分析编辑阶段
对于编译器来说,编译器只知道fruit的类型是Fruit
所以编译器在检测语法的时候,会去 Fruit.class字节码文件找到buy()方法
找到了,绑定上buy()方法,编译通过,静态绑定成功。(编译阶段属于静态绑定)
再来分析运行阶段:
运行阶段的时候,实际上在堆内存中创建的java对象是Apple/Banana对象
所以buy()的时候,真正参与buy()的对象是Apple/Banana,所以
运行阶段会动态指向Apple/Banana对象的buy()方法
这个过程属于运行阶段的绑定(运行阶段绑定属于动态绑定)
多态表示多种形态
编译的时候一种形态(编译的时候是水果)
运行的时候一种形态(运行的时候是一只苹果)
3.4.2向下类型转换
父类要用子类特有的方法
示例:
-
定义动物类
public class Animal { public void move(){ System.out.println("动物在移动"); } }
-
定义猫类
public class Cat extends Animal { //Cat子类 对父类进行重写 public void move(){ System.out.println("cat在走猫步"); } //猫除了move之外,应该有自己特有的行为(动作、方法) //例如:抓老鼠 public void zha(){ System.out.println("抓老鼠"); } }
-
测试类
public class Test01{ public static void main(String[] args) { //new 一个Animal对象 Animal animal = new Animal(); //这样就是 向上转型,子类重写父类的方法 也就是父子共有的方法 animal.move(); //如果我们访问子类特有的方法呢 zha //animal.zha(); 这样编译就有错误 //大范围(父类)--访问子类特有的方法-->小范围(子类)需要转型 Cat cat = (Cat)animal; //这样就可以 cat.zha(); } }
解释:为什么
animal.zha()
为什么会编译会报错对于编译器来说,编译器只知道animal的类型是Animal
所以编译器在检测语法的时候,会去 Animal.class字节码文件找到zha()方法
没有找到zha()方法,编译不通过,静态绑定失败。
3.5instanceof关键字
我们需要知道只要强制类型转换就会有风险,基本类型的强制类型转换会有损失精度的风险。
那么引用类型的强制类型转换也会有风险
举例:
-
定义动物类
public class Animal { public void run(){ System.out.println("动物在跑"); } }
-
定义狗类
public class Dog extends Animal { public void run(){ System.out.println("狗狗在跑步"); } }
-
定义猫类
public class Cat extends Animal{ public void run(){ System.out.println("猫猫在跑步"); } }
-
测试:
public class Test { public static void main(String[] args) { Animal animal=new Dog(); Cat cat=(Cat)animal; cat.run(); } }
-
根据测试结果:控制台输出了
java.lang.ClassCastException
类型转换异常,- 在编译的时候,我们new的是一个Animal 对象 编译通过
- 在运行的时候,Animal这个对象 底层是一个Dog类型的对象
- 在进行强制类型转换的时候 Dog和Cat没有继承关系,所以转换失败
那么我们怎么去解决呢?
instanceof关键字:
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符,它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
语法:
boolean result = 变量名 instanceof 数据类型;
意思就是 判断,这个变量是否是这个数据类型
对上方测试进行修改,也就是 判断 animal是否是一个猫 如果是一个猫 才去转型
public class Test {
public static void main(String[] args) {
Animal animal=new Dog();
if(animal instanceof Cat){
Cat cat=(Cat)animal;
}else{
System.out.println("不是该类型!");
}
}
}
第十章 final关键字
- final是java语言的一个关键字
- final表示最终的,不可变的
- final可以修饰变量以及方法,还有类等
- final修饰的局部变量,一旦赋值,不能重新赋值,final只能赋值一次
- final修饰的方法无法被覆盖(重写)
- final修饰的类无法被继承
- final修饰的实例变量必须手动赋值,也就是必须赶到系统赋默认值之前赋值
- static final联合修饰的变量为常量。常量名建议全部大写,每个单词用下划线
public static final double PI=3.1415926;
修饰类:
-
final修饰的类不能被继承。提高安全性,提高程序的可读性;
final class A{ } class B extends A{ // 错误,被final修饰的类不能被继承。 }
修饰方法:
-
final修饰的方法不能被子类重写;
class A{ public final void method(){ System.out.println("hello"); } } class B extends A{ public void method(){ // 错误,被final修饰的方法不能被重写 System.out.println("Hi"); } }
修饰变量:
-
final修饰的变量(成员变量或局部变量)只能被赋值一次;因此被final修饰的变量我们称为常量,通常全大写命名;
class A{ private final double PI = 3.14; // 声明常量 public void print(){ PI = 3.15; // 错误,被final修饰的变量是常量(不可更改值) } }