Java继承:
继承的概念:
在Java中,继承(inheritance)是面向对象编程的一个重要概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。通过继承,子类可以拥有父类的属性和方法,并且可以扩展或修改这些属性和方法,实现代码的重用和扩展。
父类怎么形成的?
当我们定义了多个类,发现每个类中的属性重复,我们就可以把这些重复属性抽取出来,形成父类,其它的类直接继承这个父类即可。
继承其实更贴合于设计这方面的感觉,因为我们写代码的时候,不可能写了好多类之后,然后发现好多类的属性重复,再去抽取,这样就很慢,而是在设计的时候就提前想好。
在Java中,使用关键字extends
来实现继承,语法如下:
class 子类名 extends 父类名 {
// 子类的属性和方法
}
继承的基本使用:
1:首先定义一个父类(people)
package Testextends;
public class People {
String name;
int age;
public void work(){
System.out.println("工作");
}
private void eat(){
System.out.println("吃饭");
}
}
2:定义若干子类(teacher,manager)
package Testextends;
public class Manager extends People{
}
package Testextends;
public class Teacher extends People{
}
同时继承父类people,不过在子类中不写任何代码
3:在main方法中创建对象
package Testextends;
public class Test01 {
public static void main(String[] args) {
Teacher teacher = new Teacher();
}
}
如图,通过java的反射机制,我们就可以看出teacher继承了people的方法,
不过我们仔细看,私有方法eat并没有显示出来。
说明子类使用不了父类的私有方法和变量,只能使用非私有方法和变量
继承中成员变量的使用:
1:继承中成员变量不重名:
首先我们需要明白一个规则,我创建了一个对象,如果我想调用这个对象里面的属性,我先是在这个对象的范围里面查找,找不到,再去查父类
明白了这个规则
我们看下面的代码:
在子类中创建一个变量zi = 10
package Testextends;
public class Teacher extends People{
int zi = 10;
}
在父类中创建一个变量fu = 100
package Testextends;
public class People {
String name;
int age;
private int gender;
int fu = 100;
public void work(){
System.out.println("工作");
}
private void eat(){
System.out.println("吃饭");
}
}
在主函数中创建父类对象
仔细看上面的代码:
父类调用子类的特有变量(zi)报错,这也印证了上面的规则。
2:继承中成员变量重名:
重名的话,很好理解,就是直接覆盖
继承中成员方法的使用:
其实和上面的差不多,如果不重名,则这个方法就算子类的特有方法
如果重名,那这个就算子类重写了父类的方法,直接覆盖掉了
注意点:
如果这个时候来一行代码:
使用父类来创建一个子类的对象
People people1 = new Teacher();
这个时候的成员变量和成员方法的结果就需要特别注意了:
package Testextends;
public class Teacher extends People{
int age = 18;
public void work(){
System.out.println("教师工作");
}
}
package Testextends;
public class People {
String name;
int age = 100;
private int gender;
public void work(){
System.out.println("工作");
}
private void eat(){
System.out.println("吃饭");
}
}
People people1 = new Teacher();
System.out.println(people1.age);
people.work();
teacher.work();
people1.work();
输出结果:
100
工作
教师工作
教师工作
我们可以发现,对于成员变量来说,age的值还是父类的值,对于成员方法来说,work方法的内容却变成了子类重写过的内容
所以我们可以总结:
继承中:
成员变量访问特点:看等号左边是谁,先调用谁中的成员变量
如上面People people1 = new Teacher();等号左边是父类(people),那成员变量的值就是左边
成员方法访问特点:看new的是谁,先调用谁中的方法
还是上面这个例子,调用的方法就是Teacher重写过的方法。
方法重写:
方法重写(Method Overriding)是指子类重写(覆盖)其父类中具有相同名称、相同参数列表和返回类型的方法。
检测是否为重写方法:在该方法上写
@Override
方法重写和方法重载的区别:
方法重载(Method Overloading)指的是在同一个类中可以有多个同名的方法,但这些方法的参数列表必须不同(包括参数的类型、顺序或个数)。在调用这些同名方法时,编译器会根据传入的参数类型来确定调用哪个重载的方法。
这是方法的重载
public class OverloadExample {
public void printInfo(int num) {
System.out.println("Number: " + num);
}
public void printInfo(String str) {
System.out.println("String: " + str);
}
public void printInfo(int num1, int num2) {
System.out.println("Numbers: " + num1 + " and " + num2);
}
public static void main(String[] args) {
OverloadExample example = new OverloadExample();
example.printInfo(10); // 调用第一个printInfo方法
example.printInfo("Hello"); // 调用第二个printInfo方法
example.printInfo(5, 8); // 调用第三个printInfo方法
}
}
这是方法的重写:
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.sound(); // 输出:Animal makes a sound
Dog dog = new Dog();
dog.sound(); // 输出:Dog barks
}
}
@Override判断此方法是否是重写方法,如果不是就会报错
重写的注意事项:
1:子类重写父类方法,权限必须要保证大于等于父类权限,这里的权限指的是访问权限
(子类中重写的方法不能使用比父类中被重写的方法更严格的访问修饰符)
权限排序:public > protected > 默认(什么都不加)> private
2:父类私有方法,构造方法,静态方法
私有方法不能继承,也不能重写
构造方法不可以继承,不能重写
静态方法可以继承,不能重写
3:子类重写父类方法之后,返回值得是父类方法返回值得子类类型
重写的使用场景:
-
实现多态性:方法重写是实现运行时多态性的一种重要手段。通过子类重写父类方法,可以在运行时根据对象的实际类型来调用相应的方法,实现动态绑定。
-
修改父类方法的行为:有时候子类可能需要修改父类方法的行为或者提供更具体的实现。通过重写父类方法,子类可以根据自己的需求来实现特定的逻辑。
-
扩展父类方法的功能:在某些情况下,子类可能需要在父类方法的基础上添加额外的逻辑或功能。通过重写父类方法并在其中调用父类的原方法,可以实现逻辑的扩展。
-
更好地适应子类的需求:通过重写父类方法,子类可以更好地适应自身的特性和需求,从而提高代码的灵活性和可维护性。
(from GPT)
super和this:
先来看一个案例:
我在这里只初始化了一个子类对象,只调用了子类的构造方法。
可是连通父类的构造方法一起调用了。
所以,我们可以得出:
1:new子类对象时,会先初始化父类(父类的无参构造方法)
2:原因:
每个构造方法的第一行,默认会有一个super(),jvm自动提供
super代表父类的无参构造
了解了这个案例之后:
super的使用:
1:概述:代表的是父类的引用
2:使用:
a.调用父类的构造方法:在子类的构造方法中使用super
关键字可以调用父类的构造方法。这样可以确保在子类对象被实例化时,父类的构造方法也会被执行。如果不显式使用super
关键字调用父类构造方法,Java编译器会默认插入调用父类的无参构造方法。
super() -> 调用父类无参构造
super(实参)->调用父类有参构造
b.调用父类的成员变量和方法:在子类中使用super
关键字可以访问调用父类的成员变量和方法
super.成员变量名
super.成员方法名(实参)
public class Fu {
int num = 10;
public Fu(){
System.out.println("我是父类中的无参构造");
}
public Fu(int data){
System.out.println("我是父类中的有参构造");
}
public void method(){
System.out.println("我是父类中的method方法");
}
}
```
```java
public class Zi extends Fu{
int num = 100;
public Zi(){
super();//调用父类中的无参构造
System.out.println("我是子类中的无参构造");
}
public Zi(int num){
super(10);//调用父类的有参构造
System.out.println("我是子类中的有参构造");
}
public void method(){
super.method();//调用父类的method方法
System.out.println("我是子类中的method方法");
System.out.println(num);//子类自己的
System.out.println(super.num);//调用父类的num
}
}
```
```java
public class Test01 {
public static void main(String[] args) {
Zi zi = new Zi();
System.out.println("============");
Zi zi1 = new Zi(10);
System.out.println("============");
Zi zi2 = new Zi();
zi2.method();
}
}
this的使用:
1.this概述:代表的是当前对象(哪个对象调用的this所在的方法,this就代表哪个对象)
当前对象:指的是指定上下文中的实例化对象
public class MyClass {
private int num;
public void setNum(int num) {
this.num = num; // 在这里,this表示当前对象的引用,指正在调用setNum方法的对象
}
}
MyClass obj = new MyClass();
obj.setNum(10);
// 当调用setNum方法时,this表示obj这个实例化的对象
这里面的obj就是当前对象
2.作用:
a.区分重名的成员变量和局部变量
b.调用当前对象中的成员
3.使用:
a.调用当前对象的构造:在构造中写
this():调用当前对象的无参构造
this(实参):调用当前对象的有参构造
public class MyClass {
private int num;
public MyClass() {
this(0); // 在这里,this表示当前对象的引用,指构造当前对象的实例
}
public MyClass(int num) {
this.num = num;
}
}
MyClass obj = new MyClass();
// 当创建MyClass对象时,this表示的是正在实例化的MyClass对象
当在主函数中实例化一个对象后,就算你调用的是一个无参构造方法, 在无参构造中调用了this(0)
还是会用那个有参的构造方法实例化。
b.调用当前对象的成员变量:
this.成员变量名
public class MyClass {
int m;
public void setNum(int num){
this.num = num;
System.out.println(this.num);
}
private int num;
public void method(){
int num = 100;
System.out.println(num);
System.out.println(this.num);
}
}
当全局变量有一个和方法中的变量重名的时候(这段代码中的num),我们就可以用this来区分全局变量和局部变量
c.将当前对象作为参数传递给其他方法:
public class MyClass {
private int num;
public void method() {
newMethod(this); // 在这里,this表示当前对象的引用,指调用method方法的对象
}
public void newMethod(MyClass obj) {
// 在这里,obj参数接收到的是this所指向的对象
}
}
MyClass obj = new MyClass();
obj.method();
// 当调用method方法时,this表示的是obj这个实例化的对象
4:注意点:
不管是super还是this,只要在构造中使用,都必须在第一行,所以二者不能同时手写出来
继承的特点:
1.继承只支持单继承,不能多继承
public class A extends B,C{} -> 错误
2.继承支持多层继承
public class A extends B{}
public class B extends C{}
3.一个父类可以有多个子类
public class A extends C{}
public class B extends C{}
继承(为父类私有属性赋值)
首先我们知道,父类的私有属性,子类是没有办法访问的,那这个时候如果我们还是要去修改值怎么办呢?
两种方法:
1:利用set赋值
其实这个就是去构造一个javabean对象,然后通过get和set方法对这个变量进行赋值
public class Employee {
private String name;
private int age;
public Employee() {
}
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void work(){
System.out.println("工作");
}
}
Teacher teacher = new Teacher();
teacher.setName("张三");
teacher.setAge(18);
System.out.println(teacher.getName()+"..."+teacher.getAge());
2:利用构造方法赋值
在子类的构造方法中利用super(参数)来构造
其实利用构造方法赋值本质上也是利用了set方法。
public class Manager extends Employee{
public Manager() {
}
public Manager(String name, int age) {
super(name, age);
}
}
Manager manager = new Manager("金莲", 24);
System.out.println(manager.getName()+"..."+manager.getAge());
继承中的抽象类:
首先我们设想一个场景:三角形类,正方形类,圆形类,他们中都有一个方法是求面积
根据我们之前学习的继承,我们很容易想到将这个求面积的方法抽象出来成一个形状类
不过我们再看,三个形状求面积的公式不同,我们不能想之前那样抽象成一个方法。
所以就需要我们的抽象类。
抽象类的定义:
Java中的抽象类是指不能被实例化的类,通常用于定义一些共同的方法和属性,让子类来实现具体的逻辑。抽象类通常包含抽象方法(没有具体实现的方法)和非抽象方法(有具体实现的方法)。
抽象类案例:
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// 抽象方法
public abstract void sound();
// 非抽象方法
public void sleep() {
System.out.println(name + " is sleeping");
}
}
在上面的例子中,Animal类是一个抽象类,包含一个抽象方法sound()和一个非抽象方法sleep()。任何继承Animal类的子类都必须实现sound()方法,否则会被标记为抽象类。
要实现一个继承自抽象类的子类,需要在子类中实现所有的抽象方法,如下所示:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void sound() {
System.out.println(name + " is barking");
}
}
在这个例子中,Dog类继承自Animal类,并且实现了抽象方法sound()。这样,Dog类就可以被实例化并调用其方法了。
特点:
- 继承了抽象类,就必须重写所有的抽象方法,由此我们也可以得出,抽象方法不能用private,final 或者static关键字来修饰,这是因为定义抽象方法的目的就是想将方法的具体实现延迟到子类,最终是要被子类重写的,而private,final,static这几个关键字都和“方法重写”的目的背道而驰。
- 拥有抽象方法的类一定是抽象类;但是抽象类不一定有抽象方法。这个如何理解,如果一个类你一开始定义为普通类,后面想用抽象方法,就会报错,必须加上abstract,但是一开始,如果你定义了一个抽象类,你可以不写抽象方法,后面再补也许
-
抽象类不能被实例化,只能创建其非抽象子类对象 。既然抽象类不能实例化,那抽象类的构造方法用处是什么呢?我们之前讲过继承可以为父类私有属性赋值,同时我们也可以利用这一点
package Testextends;
public class Manager extends Person{
public Manager(String name, int age) {
super(name, age);
}
}
package Testextends;
public abstract class Person {
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
package Testextends;
public class Test01 {
public static void main(String[] args) {
Manager manager = new Manager("张三",18);
System.out.println(manager.getName()+manager.getAge());
}
}
在这个案例中,Manager继承了抽象类Person,并且super(name, age);
就和上面那张图一样,将抽象类中的name和age属性赋上了值。
Java接口:
接口的定义:
在Java中,接口(Interface)是一种抽象数据类型,它定义了一组方法的签名,但并没有提供方法的具体实现。接口可以看作是一种约定或契约,让类来实现这些方法以确保特定的行为或功能。
来举一个生活中例子把:
当我们订购外卖时,我们可以将食品平台比作接口,而餐厅则是实现了这个接口的具体类。在这个例子中,食品平台定义了一组规范,比如订单方法、支付方法等,餐厅则按照这些规范实现了具体的行为。
接口代表食品平台提供的服务,而餐厅作为实现类,提供了具体的食品和服务。我们可以通过食品平台下单、支付等方法与不同的餐厅交互,而不需要关心具体餐厅是如何处理订单和支付的。
这样的设计使得我们可以更加灵活地选择不同的餐厅进行订餐,并且不需要知晓每个餐厅的具体细节,只需按照规范进行操作就可以了。
接口的理解(特点):
- 接口可以理解为一个抽象类,实现这个接口的类必须重写这个接口里面的所有方法,但是接口和抽象类不同的是:接口可以实现多实现,这就和之前的继承不同了,也算弥补了java不能多继承的缺点,子类可以继承一个父类的同时实现一个或者多个接口
- 接口里面的方法默认都是abstract的,在JDK8中添加了default和static,在JDK9中添加了private,如果在接口中不去特意定义default,private和static这三个关键字的话,那是不是接口中的方法默认都是abstract
- 接口中的变量默认都是静态常量,即使用
public static final
修饰的常量。 - 同时接口也不能被实例化,都只能通过子类来new
接口的使用:
关键字:
a.interface 接口
public interface 接口名{}
b.implements 实现
实现类 implements 接口名{}
第一个案例:最简单的一个接口的使用
public interface USB {
public abstract void open();
public abstract void close();
}
public class Mouse implements USB{
@Override
public void open() {
System.out.println("鼠标打开");
}
@Override
public void close() {
System.out.println("鼠标关闭");
}
}
public class Test01 {
public static void main(String[] args) {
Mouse mouse = new Mouse();
mouse.open();
mouse.close();
}
}
第二个案例:定义了默认方法:
public interface Greetable {
// 默认方法
default void greet() {
System.out.println("Hello, nice to meet you!");
}
}
public class Person implements Greetable {
public static void main(String[] args) {
Person p = new Person();
p.greet(); // 调用接口中的默认方法
}
}
对于默认方法,需要通过接口的实例来调用
第三个案例:定义了静态方法:
public interface Calculator {
// 静态方法
static int add(int a, int b) {
return a + b;
}
}
public class TestCalculator {
public static void main(String[] args) {
int sum = Calculator.add(10, 20); // 调用接口中的静态方法
System.out.println("Sum: " + sum);
}
}
静态方法直接用接口名就可以调用
第四个案例:定义了私有方法
public interface Calculation {
default int add(int a, int b) {
return performAddition(a, b); // 调用私有方法
}
private int performAddition(int a, int b) {
return a + b;
}
}
public class TestCalculation implements Calculation {
public static void main(String[] args) {
TestCalculation calc = new TestCalculation();
int sum = calc.add(10, 20); // 调用接口中的默认方法
System.out.println("Sum: " + sum);
}
}
在这个例子中,接口 Calculation
中定义了一个默认方法 add
,并在该默认方法中调用了一个私有方法 performAddition
。私有方法被用来实现具体的逻辑,但只能在接口内部使用,外部无法访问。
Java多态:
多态是指同一个方法调用,在不同对象上有不同的表现形式。
这样讲其实蛮难理解:
举一个例子:
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Dog barks");
}
void lookDoor(){
System.out.println("Dog lookDoor");
}
}
class Cat extends Animal {
void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 编译时类型为Animal,运行时类型为Dog
animal.makeSound(); // 运行时调用Dog类的makeSound方法
Animal animal = new Cat(); // 运行时类型为Cat
animal.makeSound(); // 运行时调用Cat类的makeSound方法
}
}
首先创建一个动物类Animal类,狗dog类,猫cat类
dog和cat同时继承Animal,并且重写了父类的makeSound,父类引用指向子类对象
这是我们就会发现控制台输出了:
Dog barks
Cat meows
这就是多态,相同的引用到不同的对象上,有不同的表现形式。
注意点:由父类引用指向子类对象创建出来的对象不能调用自己的特有方法
比如用Animal animal = new Dog();创建了一个对象animal,这个animal就不能调用Dog类中特有的方法lookDoor
多态的前提:
- 必须有子父类继承或者接口实现关系
- 必须有方法的重写(没有重写,多态没有意义),多态主要玩儿的是重写方法
- new对象:父类引用指向子类对象,Fu fu = new Zi() -> 理解为大类型接收了一个小类型的数据 ->比如 double b = 10
- 多态下不能直接调用子类特有功能(这一点和继承一样)
多态的条件下成员的访问特点
看new的是谁,先调用谁中的成员方法,子类没有,找父类
这一句话和继承的时候学的是一样的
比如:
成员变量的使用:
public class Fu {
int num = 1000;
}
public class Zi extends Fu{
int num = 100;
}
public class Test01 {
public static void main(String[] args) {
Fu fu = new Zi();
System.out.println(fu.num);
}
}
new的是子类,所以输出的就是子类的成员变量
成员方法的使用:
public class Fu {
int num = 1000;
public void method(){
System.out.println("我是父类中的method方法");
}
}
public class Zi extends Fu{
int num = 100;
public void method(){
System.out.println("我是子类中的method方法");
}
}
public class Test01 {
public static void main(String[] args) {
Fu fu = new Zi();
System.out.println(fu.num);//父类中的num
fu.method();//子类中重写的method方法
}
}
这里也是同理。
多态的优点:
要讲多态的优点,我们先来举个例子吧,这个例子的目的其实也不是说能说明多态的优点,就是做一个对比,已多态的方式创建对象和普通的方式创建对象
问题描述:
如果使用原始方式new对象(等号左右两边一样),既能调用重写的,还能调用继承的,还能调用自己特有的成员
但是多态方式new对象,只能调用重写的,不能直接调用子类特有的成员,那为啥还要用多态呢?
既然我们学了,那肯定就有学的用处:
先来看一下优缺点吧:
多态方式和原始方式new对象的优缺点:
原始方式:
a.优点:既能调用重写的,还能调用父类非私有的,还能调用自己特有的
b.缺点:扩展性差
多态方式:
a.优点:扩展性强
b.缺点:不能直接调用子类特有功能
这样讲肯定太宽泛了:
具体的代码案例:
package Testduotai;
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void makeSound() {
System.out.println("Cat meows");
}
}
public class Test01 {
public static void main(String[] args) {
//不用多态,用普通的方法
Dog dog = new Dog();
method01(dog);
Cat cat = new Cat();
method02(cat);
}
public static void method01(Dog dog){
dog.makeSound();
}
public static void method02(Cat cat){
cat.makeSound();
}
}
这段代码是用普通的创建对象的方式:
这段代码中:method01 和method02这两个方法的参数分别是dog和cat,
因为Dog和Cat两个对象之间没有什么关系,所以,我们需要建两个方法,方法的参数分别是dog和cat
但是如果我们用了多态的创建对象的方式:
public static void main(String[] args) {
//不用多态,用普通的方法
Dog dog = new Dog();
method01(dog);
Cat cat = new Cat();
method02(cat);
//用多态的方式
Animal animal = new Dog();
method(animal);
Animal animal1 = new Cat();
method(animal1);
}
public static void method(Animal animal){
animal.makeSound();
}
我们用一个方法就可以实现上面的功能,并且如果动物的种类多了,用这种方式可以更好的实现代码的复用性
总结起来就是:
形参传递父类类型,调用此方法父类类型可以接收任意它的子类对象
传递哪个子类对象,就指向哪个子类对象,就调用哪个子类对象重写的方法
多态中的转型
一:向上转型:
1.父类引用指向子类对象
好比是: double b = 1;
二:向下转型:
1.向下转型:好比强转,将大类型强制转成小类型
2.表现方式:
父类类型 对象名1 = new 子类对象() -> 向上转型 -> double b = 1
子类类型 对象名2 = (子类类型)对象名1 -> 向下转型 -> int i = (int)b
3.想要调用子类特有功能,我们就需要向下转型
具体讲一下第三点调用子类特有功能:
看代码:
public abstract class Animal {
public abstract void eat();
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
//特有方法
public void catchMouse(){
System.out.println("猫会捉老鼠");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗啃骨头");
}
//特有方法
public void lookDoor(){
System.out.println("狗会看门");
}
}
public class Test01 {
public static void main(String[] args) {
//多态new对象 向上转型
Animal animal = new Dog();
animal.eat();//dog重写的
//animal.lookDoor();//多态不能调用子类特有功能
//向下转型
Dog dog = (Dog) animal;
dog.eat();
dog.lookDoor();
}
}
这段代码中,猫和狗都有一个自己特定的方法捉老鼠和看门,
父类对象animal如果想要调用这两个方法,就需要向下转型
转型时会遇到的问题:
如果等号左右两边类型不一致,会出现类型转换异常(ClassCastException)
在我们进行类型转换的时候难免会发生这个异常
比如我们去调用一个别人写好的方法,我们不知道类型,我们直接进行强转,就很容易错
解决办法:
在向下转型之前,先判断类型 关键字:instanceof
判断结果是boolean型
对象名 instanceof 类型 -> 判断的是关键字前面的对象是否符合关键字后面的类型
具体看代码:
public static void method(Animal animal){
if(animal instanceof Dog){
Dog dog = (Dog) animal;
dog.lookDoor();
}
if(animal instanceof Cat){
Cat cat = (Cat) animal;
cat.catchMouse();
}
}
判断传进来的类型再进行输出。