Java-SE 面向对象
1 类和对象
1.1 类和对象的理解
类的组成:
- 属性:指事物的特征,例如:手机事物(品牌,价格,尺寸)
- 行为:指事物能执行的操作,例如:手机事物(打电话,发短信)
类和对象的关系:
- 类:类是对现实生活中一类具有共同属性和行为的事物的抽象
- 对象:是能够看得到摸的着的真实存在的实体
- 简单理解:类是对事物的一种描述,对象则为具体存在的事物
1.2 类和对象在内存上的理解
成员变量: 存储在对象的堆内存中。每个对象都有自己的堆内存空间,其中包含其成员变量的值。
方法调用: 方法的调用和执行发生在栈内存上。
1.2.1 成员变量使用:
代码运行结果:
![image-20231125162333093](https://i-blog.csdnimg.cn/blog_migrate/b83b0d50fec0f3a151164f1bbf293150.png)
1.2.2 成员方法调用:
![image-20231125162610831](https://i-blog.csdnimg.cn/blog_migrate/95ffe1aca86be2cc19294def45298e97.png)
1.2.3 多个对象的内存图
1.2.3.1 多个不同的对象:
- 成员变量使用过程
![3](https://i-blog.csdnimg.cn/blog_migrate/b3522bd6b29cf226e97a21bd43fce747.png)
- 成员方法调用过程
![4](https://i-blog.csdnimg.cn/blog_migrate/5f91480ff8162ec7b9c9bb6e01ca9b75.png)
-
总结:
多个对象在堆内存中,都有不同的内存划分,成员变量存储在各自的内存区域中,成员方法多个对象共用的一份
1.2.3.2 多个对象指向相同内存:
![4](https://i-blog.csdnimg.cn/blog_migrate/177da33fe59cc2b3d29e126b2e193c0c.bmp)
-
总结
当多个对象的引用指向同一个内存空间(变量所记录的地址值是一样的)
只要有任何一个对象修改了内存中的数据,随后,无论使用哪一个对象进行数据获取,都是修改后的数据。
1.3 成员变量和局部变量
public class VariableExample {
// 成员变量
private int memberVariable;
// 成员方法
public void exampleMethod() {
// 局部变量
int localVar1 = 10;
int localVar2 = 20;
System.out.println("Local Variable 1: " + localVar1);
System.out.println("Local Variable 2: " + localVar2);
// 使用成员变量
memberVariable = localVar1 + localVar2;
System.out.println("Member Variable: " + memberVariable);
}
public static void main(String[] args) {
// 创建对象
VariableExample exampleObject = new VariableExample();
// 调用对象的方法
exampleObject.exampleMethod();
}
}
成员变量和局部变量的区别
- 类中位置不同:成员变量(类中方法外)局部变量(方法内部)
- 内存中位置不同:成员变量(堆内存)局部变量(栈内存)
- 生命周期不同:成员变量(随着对象的存在而存在,随着对象的消失而消失)局部变量(随着方法的调用而存在,醉着方法的调用完毕而消失)
- 初始化值不同:成员变量(有默认初始化值)局部变量(没有默认初始化值,必须先定义,赋值才能使用)
1.4 方法
方法(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集
本文仅对Java中方法做大致介绍
1.4.1 无参方法
public static void method ( ) {
// 方法体;
}
1.4.2 带参方法
public static void 方法名 (参数1) {
方法体;
}
public static void 方法名 (参数1, 参数2, 参数3...) {
方法体;
}
可变参数:
在Java中,如果希望一个方法接受不确定数量的参数,可以使用可变参数(Varargs)的语法。
可变参数允许你指定一个参数,其数量可以是可变的,而不需要提前声明参数的数量。在方法定义中使用省略号 … 表示可变参数。
public static void printNumbers(int... numbers) { for (int number : numbers) { System.out.print(number + " "); } System.out.println(); } public static void main(String[] args) { // 调用方法时传入不确定数量的参数 printNumbers(1, 2, 3); printNumbers(4, 5, 6, 7, 8); printNumbers(9); }
可变参数使用的注意事项:
可变参数在方法参数列表中必须是最后一个参数,而且每个方法最多只能有一个可变参数。此外,可变参数实际上被视为数组,因此方法内部可以像处理数组一样处理可变参数。
public class VariableArgumentsExample { public static void exampleMethod(String firstArg, int... numbers) { // 方法体 } }
1.4.3 带返回值方法
public static 数据类型 方法名 ( 参数 ) {
return 数据 ;
}
1.4.4 方法的注意事项
- 方法不能嵌套定义
- void表示无返回值,可以省略return,也可以单独的书写return,后面不加数据
1.4.5 构造方法
-
功能:主要是完成对象数据的初始化
-
格式:
public class 类名{
修饰符 类名( 参数 ) {
}
}
-
注意:
-
如果没有定义构造方法,系统将给出一个默认的无参数构造方法
-
如果定义了构造方法,系统将不再提供默认的构造方法
-
如果自定义了带参构造方法,还要使用无参数构造方法,就必须再写一个无参数构造方法
-
-
建议:
-
无论是否使用,都手工书写无参数构造方法
-
可以使用带参构造,为成员变量进行初始化
-
-
setXxx和getXxx访问控制 - 封装思想
set
和get
方法通常不被归类为构造方法。它们是一种常见的编程约定,用于提供对对象的私有属性的访问和修改。
1.4.6 方法重载
1.4.6.1 方法重载概念
方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载:
- 多个方法在同一个类中
- 多个方法具有相同的方法名
- 多个方法的参数不相同,类型不同或者数量不同
1.4.6.2 方法重载注意事项:
- 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
- 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载
正确方法重载:
public class MethodDemo {
public static void fn(int a) {
//方法体
}
public static int fn(double a) {
//方法体
}
}
public class MethodDemo {
public static float fn(int a) {
//方法体
}
public static int fn(int a , int b) {
//方法体
}
}
错误的方法重载 :
public class MethodDemo {
public static void fn(int a) {
//方法体
}
public static int fn(int a) { /*错误原因:重载与返回值无关*/
//方法体
}
}
public class MethodDemo01 {
public static void fn(int a) {
//方法体
}
}
public class MethodDemo02 {
public static int fn(double a) { /*错误原因:这是两个类的两个fn方法*/
//方法体
}
}
方法重载避免了过度的方法命名,提高了代码的可读性和简洁性,使得方法的参数的类型及个数更加的灵活
1.4.7 方法的参数传递
核心:
- 基本数据类型的参数,形式参数的改变,不影响实际参数
- 对于引用类型的参数,形式参数的改变,影响实际参数的值
1.4.7.1 基本数据类型参数
基本数据类型的参数,形式参数的改变,不影响实际参数
依据:每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失
1.4.7.2 应用数据类型参数
引用类型的参数,形式参数的改变,影响实际参数的值
依据:引用数据类型的传参,传入的是地址值,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果
2 面向对象三大特征 - 封装,继承,多态
2.1 封装
- 定义: 封装是将数据和方法(行为)打包到一个单一的单元(类)中,同时对外部隐藏数据的实现细节。
- 实现: 使用私有化成员变量,通过公共的方法(getter和setter)来访问和修改数据。这样可以防止直接访问对象的内部状态,强调了对象内部和外部的隔离。
2.2 继承
2.2.1 继承的概念
继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,以及追加属性和方法
2.2.2 实现继承的格式
-
继承通过
extends
实现 -
格式:class 子类 extends 父类 { }
public class Fu { public void show() { System.out.println("show方法被调用"); } } public class Zi extends Fu { public void method() { System.out.println("method方法被调用"); } } public class Demo { public static void main(String[] args) { //创建对象,调用方法 Fu f = new Fu(); f.show(); Zi z = new Zi(); z.method(); z.show(); } }
2.2.3 继承的好处
- 继承可以让类与类之间产生关系,子父类关系,产生子父类后,子类则可以使用父类中非私有的成员。
- 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
- 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
2.2.4 继承的弊端
- 继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性
2.2.5 继承的应用场景
使用继承,需要考虑类与类之间是否存在is..a
的关系,不能盲目使用继承
is…a的关系:谁是谁的一种,例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类
2.2.6 继承中成员的访问
2.2.6.1 变量的访问
在子类方法中访问一个变量,采用的是就近原则。
-
子类局部范围找
-
子类成员范围找
-
父类成员范围找
-
如果都没有就报错(不考虑父亲的父亲)
Super
this&super关键字:
- this:代表本类对象的引用
- super:代表父类存储空间的标识(可以理解为父类对象引用)
this和super的使用分别
- 成员变量:
- this.成员变量 - 访问本类成员变量
- super.成员变量 - 访问父类成员变量
- 成员方法:
- this.成员方法 - 访问本类成员方法
- super.成员方法 - 访问父类成员方法
构造方法:
- this(…) - 访问本类构造方法
- super(…) - 访问父类构造方法
super的内存图
**核心:**对象在堆内存中,会单独存在一块super区域,用来存放父类的数据
2.2.6.2 构造方法的访问
注意:子类中所有的构造方法默认都会访问父类中无参的构造方法
子类会继承父类中的数据,可能还会使用父类的数据。所以,子类初始化之前,一定要先完成父类数据的初始化,原因在于,每一个子类构造方法的第一条语句默认都是:super()
提问:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?
1. 通过使用super关键字去显示的调用父类的带参构造方法 2. 在父类中自己提供一个无参构造方法
最好的办法仍是自己给出无参构造方法
2.2.6.3 成员方法的访问
当我们通过子类对象访问一个方法时
- 子类成员范围找
- 父类成员范围找
- 如果都没有就报错(不考虑父亲的父亲…)
2.2.6.4 方法重写
1、概念
- 方法重写是指在子类中重新定义(覆盖)父类中已经存在的方法
2、应用场景
-
当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容
// 父类 class Animal { // 方法被子类重写 public void makeSound() { System.out.println("Animal makes a sound"); } } // 子类 class Dog extends Animal { // 重写父类的方法 @Override public void makeSound() { System.out.println("Dog barks"); } } // 主方法用于测试 public class InheritanceExample { public static void main(String[] args) { // 创建子类对象 Dog myDog = new Dog(); // 调用被重写的方法 myDog.makeSound(); } }
3、Override注解
- 用来检测当前的方法,是否是重写的方法,起到【校验】的作用
2.3 多态
2.3.1 多态的概述
-
什么是多态
同一个对象,在不同时刻表现出来的不同形态
-
多态的前提
重点
- 要有继承或实现关系
- 要有方法的重写(重写父类中的方法,不一定要全部重写,但补不重写,无意义)
- 要有父类引用指向子类对象
2.3.2 多态中的成员访问特点 重点
-
成员访问特点
-
成员变量
编译看父类,运行看父类:编译和运行时期都是参考引用变量所属类中的成员,且在编译期间,如果没有,则编译失败,所以也不能使用子类的特有成员
-
成员方法
编译看父类,运行看子类:当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法
-
-
代码演示
-
动物类
public class Animal { public int age = 40; public void eat() { System.out.println("动物吃东西"); } }
-
猫类
public class Cat extends Animal { public int age = 20; public int weight = 10; @Override public void eat() { System.out.println("猫吃鱼"); } public void playGame() { System.out.println("猫捉迷藏"); } }
-
测试类
public class AnimalDemo { public static void main(String[] args) { //有父类引用指向子类对象 Animal a = new Cat(); System.out.println(a.age); //输出40 // System.out.println(a.weight);//编译失败 a.eat();//输出猫吃鱼 // a.playGame();//编译失败 } }
-
2.3.3 多态的好处和弊端重点
-
好处
提高程序的扩展性。定义方法时候,使用父类型作为参数,在使用的时候,使用具体的子类型参与操作
-
弊端
不能使用子类的特有成员
2.3.4 多态中的转型 重点
-
向上转型 (多态中的默认过程)
父类引用指向子类对象就是向上转型
-
向下转型 (强制过程)
格式:子类型 对象名 = (子类型)父类引用;
-
代码演示
- 动物类
public class Animal { public void eat() { System.out.println("动物吃东西"); } }
- 猫类
public class Cat extends Animal { @Override public void eat() { System.out.println("猫吃鱼"); } public void playGame() { System.out.println("猫捉迷藏"); } }
- 测试类
public class AnimalDemo { public static void main(String[] args) { //多态 //向上转型 Animal a = new Cat(); a.eat(); // a.playGame(); //向下转型 Cat c = (Cat)a; c.eat(); c.playGame(); } }
注意:必须保证对象在创建的时候就是猫,向下转型才可以成为猫;否则,如果最开始为狗,如果要将其强制转换为猫,就会报错。
3 抽象类与接口
3.1 抽象类
当我们在做子类共性功能抽取时,有些方法在父类中并没有具体的实现,这个时候就需要抽象类
在Java中,一个没有方法体的方法应该定义为抽象方法,而类中如果有抽象方法,该类必须定义为抽象类
3.1.1 抽象类特点
-
抽象类和抽象方法必须使用 abstract 关键字修饰
//抽象类的定义 public abstract class 类名 {} //抽象方法的定义 public abstract void eat();
-
抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
-
抽象类不能实例化,参照多态的方式,通过子类对象实例化,这叫抽象类多态
-
抽象类的子类
要么重写抽象类中的所有抽象方法;否则自身也是是抽象类
3.1.2 抽象类成员特点
3.2 接口
Java中的接口更多的体现在对行为的抽象
3.2.1 接口的特点
-
接口用关键字
interface
修饰public interface 接口名 {}
-
类实现接口用
implements
表示public class 类名 implements 接口名 {}
-
接口不能实例化
参照多态的方式,通过实现类对象实例化,这叫接口多态。
多态的形式:具体类多态,抽象类多态,接口多态。
-
接口的子类
要么重写接口中的所有抽象方法
要么子类也是抽象类
3.2.2 接口的成员特点
接口中的常量是不能修改的
3.2.3 类和接口的关系 重点
-
类与类的关系:
继承关系,只能单继承,但是可以多层继承
-
类与接口的关系
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
-
接口与接口的关系
继承关系,可以单继承,也可以多继承
// 定义第一个接口 interface InterfaceA { void methodA(); } // 定义第二个接口 interface InterfaceB { void methodB(); } // 定义继承自两个接口的新接口 interface CombinedInterface extends InterfaceA, InterfaceB { void combinedMethod(); }
3.2.4 抽象类和接口的区别 重点
-
成员区别
-
抽象类
变量,常量;有构造方法;有抽象方法,也有非抽象方法
-
接口
常量;抽象方法
-
-
关系区别
-
类与类
继承,单继承
-
类与接口
实现,可以单实现,也可以多实现
-
接口与接口
继承,单继承,多继承
-
-
设计理念区别
-
抽象类
对类抽象,包括属性、行为
-
接口
对行为抽象,主要是行为
-
4 参数传递
4.1 类名作为形参和返回值
- 类名作为方法的形参
- 方法的形参是类名,其实需要的是该类的对象
- 实际传递的是该对象的【地址值】
- 类名作为方法的返回值
- 方法的返回值是类名,其实返回的是该类的对象
- 实际传递的,也是该对象的【地址值】
4.2 抽象类作为形参和返回值
- 抽象类作为形参和返回值
- 方法的形参是抽象类名,其实需要的是该抽象类的子类对象
- 方法的返回值是抽象类名,其实返回的是该抽象类的子类对象
4.3 接口名作为形参和返回值
- 接口作为形参和返回值
- 方法的形参是接口名,其实需要的是该接口的实现类对象
- 方法的返回值是接口名,其实返回的是该接口的实现类对象
5 内部类
5.1 内部类的基本使用
-
内部类概念
在一个类A的内部定义一个类B,类B就被称为内部类
-
内部类定义格式
/* 格式: class 外部类名{ 修饰符 class 内部类名{ } } */ class Outer { public class Inner { } }
-
内部类的访问特点
重点
- 内部类可以直接访问外部类的成员,包括私有
- 外部类要访问内部类的成员,必须创建对象
-
示例代码:
/* 内部类访问特点: 内部类可以直接访问外部类的成员,包括私有 外部类要访问内部类的成员,必须创建对象 */ public class Outer { private int num = 10; public class Inner { public void show() { System.out.println(num); } } public void method() { Inner i = new Inner(); i.show(); } }
5.2 成员内部类
-
成员内部类的定义位置
- 在类中方法,跟成员变量是一个位置
-
外界创建成员内部类格式
- 格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象;
- 举例:
Outer.Inner oi = new Outer().new Inner();
-
成员内部类的推荐使用方案
- 将一个类,设计为内部类的目的,大多数都是不想让外界去访问,所以内部类的定义应该私有化,私有化之后,再提供一个可以让外界调用的方法,方法内部创建内部类对象并调用。
-
示例代码:
class Outer { private int num = 10; private class Inner { public void show() { System.out.println(num); } } public void method() { Inner i = new Inner(); i.show(); } } public class InnerDemo { public static void main(String[] args) { //Outer.Inner oi = new Outer().new Inner(); //oi.show(); Outer o = new Outer(); o.method(); } }
5.3 局部内部类
-
局部内部类定义位置
- 局部内部类是在方法中定义的类
-
局部内部类方式
- 局部内部类,外界是无法直接使用,需要在方法内部创建对象并使用
- 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
-
示例代码
class Outer { private int num = 10; public void method() { int num2 = 20; class Inner { public void show() { System.out.println(num); System.out.println(num2); } } Inner i = new Inner(); i.show(); } } public class OuterDemo { public static void main(String[] args) { Outer o = new Outer(); o.method(); } }
5.4 匿名内部类
-
匿名内部类的前提
- 存在一个类或者接口,这里的类可以是具体类也可以是抽象类
-
匿名内部类的格式
-
格式:
new 类名 ( ) { 重写方法 } new 接口名 ( ) { 重写方法 }
-
举例:
new Inter(){ @Override public void method(){} }
-
-
匿名内部类的细节
-
匿名内部类可以通过多态的形式接受
Inter i = new Inter(){ @Override public void method(){ } }
-
-
匿名内部类直接调用方法
interface Inter{ void method(); } class Test{ public static void main(String[] args){ new Inter(){ @Override public void method(){ System.out.println("我是匿名内部类"); } }.method(); // 直接调用方法 } }
5.5 匿名内部类在开发中的使用
-
匿名内部类在开发中的使用
-
通常用于简化代码
当发现某个方法需要,接口或抽象类的子类对象,我们就可以传递一个匿名内部类过去,来简化传统的代码
-
-
示例代码:
interface Jumpping { void jump(); } class Cat implements Jumpping { @Override public void jump() { System.out.println("猫可以跳高了"); } } class Dog implements Jumpping { @Override public void jump() { System.out.println("狗可以跳高了"); } } class JumppingOperator { public void method(Jumpping j) { //new Cat(); new Dog(); j.jump(); } } class JumppingDemo { public static void main(String[] args) { //需求:创建接口操作类的对象,调用method方法 JumppingOperator jo = new JumppingOperator(); Jumpping j = new Cat(); jo.method(j); Jumpping j2 = new Dog(); jo.method(j2); System.out.println("--------"); // 匿名内部类的简化 jo.method(new Jumpping() { @Override public void jump() { System.out.println("猫可以跳高了"); } }); // 匿名内部类的简化 jo.method(new Jumpping() { @Override public void jump() { System.out.println("狗可以跳高了"); } }); } }