抽象类
抽象类的概念
Java中使用类实例化对象描述现实生活中的实体,但是并不是所有的类都可以描述一个实体,如果一个类没有包含足够的信息来描述一个具体的类,那么这个类就是抽象类,或者说,将这样的类设计为抽象类。
我们来看下面的一段代码:
// Test.java
public class Test {
public static void main(String[] args) {
Flower flower = new Flower();
Round round = new Round();
Shape[] shapes = {flower,round};
for (Shape shape:shapes) {
shape.draw();
}
}
}
// Flower.java
public class Flower extends Shape{
public void draw() {
System.out.println("花一朵❀");
}
}
// Round.java
public class Round extends Shape{
public void draw() {
System.out.println("画一个圆形");
}
}
// Shape.java
public class Shape {
public void draw(){
System.out.println("打印一个图形");
};
}
当我们实现Shape类的draw方法时,由于Shape的含义比较的宽泛,不是可以用具体的图形去描述,因此对应Shape来说,可以将它设置为抽象类,被其他类所继承。
抽象类的语法
1.抽象方法中不能有主体
在Java中被abstract修饰的类被称之为抽象类,抽象类中被abstract修饰的方法称之为抽象方法。
在上述的代码中,执行的结果是花一朵❀,画一个圆形,并没有打印一个图形,那么如果我们将Shape写成一个抽象方法,那么内部还需要具体的实现吗?不需要,抽象类中并不需要具体的实现。将Shape写作抽象方法代码如下:
public abstract class Shape {
public abstract void draw();
}
但是如果我们将抽象方法进行实现,那么就会出现如下的报错:
即抽象方法中不能有主体。
注意:抽象方法中可以有普通的成员变量和成员方法或者是构造方法。
如果一个类中有抽象方法,这个类必须是抽象类。否则就会报错
抽象类和普通类的区别:抽象类有abstract修饰,并且可以存在抽象方法(也可以不存在)
2.抽象类不能被private,final,static修饰
- private表示类,方法或者变量是私有的,只能在定义的类中去访问,但是抽象类需要被其他的子类访问因此不可行。
- final修饰的类表示该类不能有子类,或者说不能被继承,这样抽象类相违背,因此不可行。
- 被static修饰表示不能被实例化,但是子类是抽象类的实例化,因此不可行。
3.抽象类不能实例化对象
由于抽象类去定义一类具体事物的抽象,只是提供了一些共有的属性,不包含像具体的类那样丰富的信息去实例化一个对象。就想是买一件衣服付款时,由于衣服可看作一个抽象类,不同的衣服的款式,尺寸,知名度都会影响它的价格,因此不能用一份这个抽象类去实例化一件具体衣服的价格。否则就会报如下错误:
但是抽象类的引用 可以引用其子类的对象。代码如下:
public abstract class Shape {
public abstract void draw();
}
public class Cycle extends Shape{
public void draw() {
System.out.println("画一个圆形...");
}
}
public class triangle extends Shape{
public void draw () {
System.out.println("画一个三角形");
}
}
public class test {
public static void main(String[] args) {
Cycle cycle = new Cycle();
triangle triangle = new triangle();
Shape[] shapes = {cycle,triangle};
for (Shape shape:shapes) {
shape.draw();
}
// 向上转型
Shape shape = new Cycle();
}
}
4.继承抽象类的子类必须重新抽象方法,如果不想重新写,那么这个类必须是抽象类,如果下一个类继承了第二个抽象类,那么他必须重写两个类的抽象方法。例如:
public abstract class Shape {
public abstract void draw();
}
abstract class A extends Shape {
abstract void test();
}
class C extends A {
void test() {
System.out.println("这是重写的第二个抽象类的抽象方法");
}
public void draw() {
System.out.println("这是重写的第一个抽象类的抽象方法");
}
}
5.抽象类可不包含抽象方法,但是抽象方法必须在抽象类中
6.抽象类可以存在构造方法,用于子类实例化时初始化父类的成员
public abstract class Shape {
public int a;
// 抽象类构造方法 -- 构造方法必须与类目相同,并没有返回类型
public Shape(int a) {
this.a = a;
}
public abstract void draw();
}
public class Cycle extends Shape{
public Cycle(int a) {
// super调用父类的构造方法
super(a);
}
public void draw() {
System.out.println("画一个圆形...");
}
}
public class test {
public static void main(String[] args) {
// 发生向上转型,初始化父类成员a
Shape shape = new Cycle(1);
System.out.println(shape.a);
}
}
7.抽象类可以发生动态绑定
public abstract class Shape {
public abstract void draw();
}
public class Cycle extends Shape{
public void draw() {
System.out.println("画一个圆形...");
}
}
public class Triangle extends Shape{
public void draw () {
System.out.println("画一个三角形");
}
}
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Triangle triangle = new Triangle();
Cycle cycle = new Cycle();
triangle.draw();
cycle.draw();
//-------------------------------------
new Triangle().draw();
new Cycle().draw();
//-------------------------------------
drawMap(triangle);
drawMap(cycle);
}
}
-
当您调用triangle.draw()和cycle.draw()时,虽然triangle和cycle都是Shape类型的引用,但是由于它们分别指向Triangle和Cycle类的实例,调用相应类的draw方法。发生动态绑定。
-
使用匿名对象调用方法,例如new Triangle().draw();和new Cycle().draw();发生动态绑定。在运行时,JVM会根据匿名对象实际类型调用相应的方法。
-
方法drawMap(Shape shape)接受一个Shape类型的参数,但实际传入的是triangle和cycle类的实例。在drawMap方法内部调用shape.draw();时,动态绑定确保调用的是传入对象实际类型的draw方法。
抽象类的意义
我们可能有这样的疑问,普通类可以被继承,普通的成员方法可以被重写,那么为什么使用抽象类和抽象方法呢?
抽象类中的抽象方法如果没有被重写,编译器会报错。 是的,使用抽象类和抽象方法的好处就是多了一层编译器的校验,能帮助我们检查抽象方法是否被重写。
抽象类不能被实例化。 很多引用场景,比如上面的画图形场景,其实际工作不应该由父类完成,而应由子类完成,此时误用父类编译器会报错。
接口
接口的概念
接口在Java中是一个抽象类型,是抽象方法的集合,一个类通过继承接口的方式,从而继承接口的抽象方法。例如我们可以将图形类写作图形接口。
接口可以看作多个类的公共规范,是一种引用类型
接口的语法
接口的定义与类的定义类似,只需要把class换成interface
接口能被public,abstract修饰,abstract默认不显性显示,如果省略public 则使用默认的权限
public interface 接口名 {
// ...
}
- 接口的命名一般以I开头
- 接口的命名一般使用形容词性单词,这样就可以显示出接口的标准
接口的特性
1.接口不能被实例化
接口不能被实例化,但是接口可以引用实现该接口的类的对象(向上转型)
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
animal.eat();
}
}
public interface Animal {
void eat();
}
public class Cat implements Animal{
public void eat() {
System.out.println(this.name+"正在吃🐟");
}
}
打印的结果是小猫正在吃🐟。 如果写为Animal animal = new Animal();那么就会:
2.接口的成员变量
接口可以初始化变量,且接口的成员变量都是public static final修饰的,即使不写修饰符,成员变量默认被public static final修饰,这就意味着接口中的成员变量是静态(被static修饰)的,并且在声明的时候必须是初始化,因为它们被视为常量(被final修饰),这是可以将接口中的成员变量看作静态常量,只能读取,但是不能够被修改。
public interface Animal {
int age = 10;
public static final String name = "小动物";
}
public class Cat implements Animal{
String name = "小猫";
int age = 2;
}
public class Dog implements Animal{
String name = "小狗";
int age = 3;
}
public class Test {
public static void main(String[] args) {
Animal animal = new Cat(); // 向上转型
animal.age = "5"; // 错误的,常量不可以被修改
}
}
看到上述代码,发现name和age被public static final 修饰,但是为什么在Cat和Dog类进行了“修改”,这是因为在Cat和Dog这两个类中,name和age不是接口Animal中常量的副本,而是Cat和Dog类自己的实例,这些实例是可以被修改的。
但是在main函数中animal.age这个就是对于Animal接口中的age进行修改了,因此这句话是错误的。
3.接口的方法
3.1接口中的抽象方法
接口定义的方法默认被public abstract修饰,属于抽象方法,所以它们没有方法体。抽象方法用来规定接口的类中必须实现的方法,任何实现这个接口的类必须实现其中的抽象方法
public interface Animal {
int age = 10;
public static final String name = "小动物";
void sleep();
public abstract void eat();
}
public class Cat implements Animal{
String name = "小猫";
int age = 2;
public void eat() {
System.out.println(this.name+"正在吃🐟");
}
public void sleep() {
System.out.println(this.name+"正在睡觉");
}
}
public class Dog implements Animal{
String name = "小狗";
int age = 3;
public void eat() {
System.out.println(this.name+"正在吃骨头");
}
public void sleep() {
System.out.println(this.name+"正在睡觉");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
cat.sleep();
}
}
结果:
如何不实现就会报错:
3.2接口中的默认方法
如果是要接口中的方法有具体的实现,可以使用默认方法,默认方法用default关键词定义,如果一个类实现了包含默认方法的接口,如果在这个类中没有覆盖这个方法,那么就会继承这个默认的实现。如果进行了覆盖,那么使用覆盖的方法。
例子,Cat类进行了覆盖以及不覆盖测试:
public interface Animal {
default void sound() {
System.out.println("小动物在叫");
}
}
public class Cat1 implements Animal{
// 没有实现sound()
}
public class Cat2 implements Animal{
// 实现sound()
public void sound() {
System.out.println(this.name+"正在咪咪叫");
}
}
public class Test {
public static void main(String[] args) {
Cat1 cat1 = new Cat1();
cat1.sound();
Cat2 cat2 = new Cat2();
cat2.sound();
}
}
结果如下:
注意,这组代码是分开运行的,当其中一个实现的了sound()时,另外一个调用时被实现的sound(),而不是animal中的sound();
3.3接口中的静态方法
- 静态方法可以直接被调用,不需要实例化
- 接口中的静态方法不能被实现该接口的类重写
public interface Animal {
static void run() {
System.out.println("小动物正在跑");
}
}
package demo9;
public class Cat implements Animal{
public void run() {
System.out.println("小猫正在跑");
}
}
public class Test {
public static void main(String[] args) {
Animal.run();
Cat cat = new Cat();
cat.run();
}
}
run()方法在Cat类中并不是重写接口的静态方法,而是Cat类的一个实例方法。在Java中,接口的静态方法不是抽象方法,因此不需要也不可能在实现类中被重写。
4.抽象类继承抽象接口
当抽象类继承了抽象接口,抽象类可以不重写抽象接口中的方法,但是如果一个非抽象类继承了该抽象方法,那么该抽象类需要重写上面所有的抽象方法。例如:
// 猫科
public interface feline {
void eat();
void sleep();
}
// 猫属
abstract class Felis implements feline{
abstract void Fsleep();
public void MyFeils() {
System.out.println("猫属中的特有方法");
}
}
// 猫
public class Cat extends Felis{
public void sleep() {
System.out.println("实现猫科的sleep()");
}
public void eat() {
System.out.println("实现猫科的eat()");
}
void Fsleep() {
System.out.println("实现猫属的Fsleep()");
}
}
public static void main(String[] args) {
Cat myCat = new Cat();
myCat.eat();
myCat.sleep();
myCat.Fsleep();
myCat.MyFeils();
}
5.重写后的权限
抽象方法被实例化后一定要重写,由于接口的成员方法默认都有public修饰,所有重写后的方法必须时public修饰(重写后的访问权限大于等于父类)
6.接口中不能有静态代码块,构造代码块和构造方法
-
静态代码块: 静态代码块是在类加载时执行的代码块,通常用于初始化静态变量。接口中不能有静态代码块,因为接口不涉及对象的实例化过程,所以不需要在加载时执行特定的代码。
-
构造代码块: 构造代码块在类的每个实例创建时执行,用于初始化实例变量。接口中不能有构造代码块,因为接口不能被实例化。
-
构造方法: 构造方法用于创建类的新实例。接口中不能有构造方法,因为接口只定义了可以由实现它的类来实现的方法,本身并不实现这些方法,也不需要创建实例。
注意:接口文件的文件名必须与接口名相同,且接口文件编译后也是.class为后缀的字节码文件
接口的使用
接口不能直接实例化对象,接口的使用需要一个“实现类”来实现接口中的方法,用到关键字implement
接口与类之间时实现关系,父类和子类时继承关系。继承表达的时is A的关系,例如猫是动物,接口表达的时具有xxx含义,例如猫会爬树。下面用键盘和鼠标的例子来演示:
public interface USB {
void openDevice();
void closeDevice();
}
public class Mouse implements USB{
public void openDevice() {
System.out.println("打开鼠标");
}
public void closeDevice() {
System.out.println("关闭鼠标");
}
public void click() {
System.out.println("点击鼠标");
}
}
public class KeyBoard implements USB{
public void openDevice() {
System.out.println("打开键盘");
}
public void closeDevice() {
System.out.println("关闭键盘");
}
public void input() {
System.out.println("打字");
}
}
public class Computer {
public void open() {
System.out.println("开机");
}
public void close() {
System.out.println("关机");
}
public void useUSBDevice(USB usb) {
usb.openDevice();
if(usb instanceof Mouse) {
Mouse mouse = (Mouse) usb;
mouse.click();
}else {
KeyBoard keyBoard = (KeyBoard) usb;
keyBoard.input();
}
usb.closeDevice();
}
}
public class Test {
public static void main(String[] args) {
Computer computer = new Computer();
computer.open();
computer.useUSBDevice(new KeyBoard());
computer.useUSBDevice(new Mouse());
computer.close();
}
}
多个接口的实现
在Java中,类和类之间时单继承的,一个类只有一个父类,即Java中不支持多继承。看一个例子:对于一个动物来说,不仅仅只有走路,有的还会游泳,甚至一些还会飞,我们应该如何表示,这时就要引出Java中的一个类实现多个接口,这个类要实现所有接口的抽象方法。
一个类可以继承一个父类并实现多个接口,当继承和接口都存在时,先使用extend进行继承,然后使用implements实现多个接口
public interface IWalk {
void walk();
}
public interface ISwimming {
void swimming();
}
public class Animal {
public String name;
public int age;
}
public class Dog extends Animal implements IWalk,ISwimming{
public Dog(String name,int age) {
this.age = age;
this.name = name;
}
public void walk() {
System.out.println(this.name+"正在跑");
}
public void swimming() {
System.out.println(this.name+"正在刨水");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("旺财",'4');
dog.swimming();
dog.walk();
}
}
接口与多态
看下面一组代码:
public interface IFly {
void fly();
}
public interface IWalk {
void walk();
}
public interface ISwimming {
void swimming();
}
public abstract class Animal {
public String name;
public int age;
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public abstract void eat();
}
public class Duck extends Animal implements IWalk,ISwimming,IFly{
public Duck(String name,int age) {
super(name,age);
}
@Override
public void eat() {
System.out.println(this.name+"正在吃鸭粮");
}
@Override
public void fly() {
System.out.println(this.name+"正在飞");
}
@Override
public void swimming() {
System.out.println(this.name+"正在游泳");
}
@Override
public void walk() {
System.out.println(this.name+"正在走路");
}
}
public class Dog extends Animal implements ISwimming,IWalk{
// 子类继承父类先实现父类的构造方法
public Dog(String name,int age) {
super(name,age);
}
@Override
public void eat() {
System.out.println(this.name+"吃狗粮");
}
public void bark() {
System.out.println(this.name+"正在汪汪叫...");
}
@Override
public void swimming() {
System.out.println(this.name +"正在游泳");
}
public void walk() {
System.out.println(this.name+"正在跑");
}
}
public class Fish extends Animal implements ISwimming{
public Fish(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(this.name+"正在吃🐟粮");
}
@Override
public void swimming() {
System.out.println(this.name+"正在游泳");
}
}
public class Test {
public static void func1(Animal animal) {
animal.eat();
}
public static void walk(IWalk iWalk) {
iWalk.walk();
}
public static void fly(IFly iFly) {
iFly.fly();
}
public static void swimming(ISwimming iSwimming) {
iSwimming.swimming();
}
public static void main(String[] args) {
walk(new Duck("小鸭子",3));
walk(new Dog("旺财",4));
}
// animal调用对象不同,发生的行为不一样,发生动态绑定,发生了多态
public static void main1(String[] args) {
func1(new Duck("小鸭子",3));
func1(new Dog("旺财",4));
func1(new Fish("金鱼",2));
}
}
上面的代码中不同的子类继承了父类并实现了接口,在animal进行调用时,调用的对象不同,发生的行为不同,发生了动态绑定,发生了多态;在main方法中,我们创建了Duck,Dog的实例,并将它们作为参数传递给对应的方法。由于这些类实现了相应的接口,所以它们可以被传递给这些方法,并调用它们实现的接口方法。
当有了接口之后,类的使用者就不必关注具体类型,而是关注某个类是否具备某种能力。
接口间的扩展
一个接口继承(扩展)另一个接口,子接口不实现父接口的抽象方法,而当一个非抽象类实现了子接口,那么这里类要重写父接口的所有抽象方法。接口之间的继承相当于合并接口。例如:
interface IA {
void testA();
}
interface IB{
void testB();
}
interface IC extends IA,IB {
void testC();
}
class D implements IC {
@Override
public void testA() {
}
@Override
public void testB() {
}
@Override
public void testC() {
}
}
接口之间可以实现多继承,例如两栖动物可以行走可以游泳:
public interface IRunning {
void run();
}
public interface ISwimming {
void swim();
}
public interface IAmphibious extends ISwimming, IRunning {
void func();
}
抽象类和接口的区别
抽象类中可以包含普通方法和字段,这些方法可以被子类直接使用(可以不重写),而接口中不能定义普通的字段和方法,子类必须重写接口中的所有抽象方法。
- 抽象类可以有方法体,但是接口中不行,除非用default和static进行修饰。
- 一个类只能继承一个抽象类,但是一个类可以有多个接口
- 抽象类中成员变量可以是各种类型,但是接口中public static final 类型
- 抽象类中可以有静态代码块和静态方法,接口中不能有静态代码块和静态方法(Java8开始有静态方法)。
区别 | 抽象类 | 接口 | |
1 | 结构组成 | 普通类,抽象方法 | 抽象方法,全局常量 |
2 | 权限 | 各种权限 | public |
3 | 子类使用 | extends关键字继承抽象类 | implements关键字实现接口 |
4 | 关系 | 一个抽象类可以实现多个接口 | 接口不能继承抽象类,但是接口可以使用extends关键字继承多个父接口 |
5 | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
实现接口和继承的区别:
- 接口中的抽象方法必须重写,而继承中的子类可以不重写父类中的方法
- 一个类只能继承一个父类,一个方法可以实现多个接口
- 接口中只有抽象方法和被public static final修饰的成员变量,继承中可以定义普通的属性和方法
- 接口--implement,继承--extend
Object类:
在Java中,object类时所有类的父类。每个Java类都是直接或间接的继承object类,如果没有明确指定父类,则默认继承自object类。
toString():
返回对象的字符串表示形式,每个对象都有一个toString(),方法,默认情况下,toString()返回的字符串包含类的名称+@+对象的哈希码无符号的十六进制。例如:
public class Person {
public String name;
public int age;
public Person(String name,int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Person person = new Person("小明",16);
System.out.println(person);
}
}
打印:
但是如果我们重写了toString(),就会打印出正确的值。
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
equals():
public static void main(String[] args) {
Dog obj1 = new Dog("旺财", 3);
Dog obj2 = new Dog("旺财", 3);
System.out.println(obj1 == obj2); // false,因为比较的是引用
System.out.println(obj1.equals(obj2)); // false
}
明明两个对象相同,但是为什么时false呢,对于第一个比较,==是比较的是两个对象的引用,而不是比较的对象的内容,即使它们的内容相同,但是它们在内存中的地址不同,因此obj1==obj2结果是false,对于第二个比较,还是比较两个对象的引用是否相同。如果我们需要比较内容相同,就必须重写equals方法。
public boolean equals(Object obj) {
if (this == obj) return true;
if(obj == null) {
return false;
}
if(!(obj instanceof Dog)) {
return false;
}
// 传入的对象转化为dog类型
Dog tmp = (Dog) obj;
// 字符串比较equals
return tmp.name.equals(this.name) && tmp.age == this.age;
}
由于Dog是Object的子类,子类重写父类的方法,子类调用时调用自己的方法,即调用自己重写的方法
hashCode():
public static void main(String[] args) {
Dog dog1 = new Dog("旺财",3);
Dog dog2 = new Dog("旺财",3);
System.out.println(dog1.hashCode());
System.out.println(dog2.hashCode());
}
hashCode()可以提高数据的检索效率,并保证数据结构的重要性,由于并不是所有的类都需要被当作哈希表的间,因此在Java中没有实现。需要自己去实现hashCode()
我们这里需要输出的相同,因为name和age相同
@Override
public int hashCode() {
return Objects.hash(name,age);
}