文章目录
抽象类
什么是抽象类?
在Java中,并不是所有的对象都是通过类来进行描述的,在有些时候,一个类中并没有包含足够的信息来描绘一个具体的对象,类似这样的类就是抽象类。
常见的,如果我们在类中写了一个方法,但是这个方法并没有具体的实现细节(不给出具体的实现体),那么我们就可以将这个方法设计成一个抽象方法,那么包含抽象方法的类就可以称为抽象类。
abstract class A{
//被abstract修饰的方法没有方法体
abstract public void eat();
//抽象类也是类,可以添加普通变量和方法
private String name;
public void sleep(){
System.out.println("666");
}
//抽象类也是可以构造方法
public A(String name) {
this.name = name;
}
}
注意:
- 使用abstract修饰方法的就是抽象方法,使用abstract修饰的类就是抽象类。
- 抽象类也是类,内部既可以包含抽象方法,也可以包含普通方法和属性,甚至是构造方法等。
抽象类的特性
- 抽象类不能直接实例化对象,需要子类继承这个抽象类,再实例化这个子类
- 在之前我们讲过,如果不加访问限定符的话,会默认是包访问权限;但是如果是抽象方法在没有加访问限定符,默认则是public
- 抽象类一般来说,是必须被继承的,并且继承后子类要重写父类中的所有抽象方法,否则子类也必须是抽象类(就是必须被abstract修饰的);若两样都不具备,编译器则会直接报错
abstract class A{
//被abstract修饰的方法没有方法体
abstract public void eat();
//抽象类也是类,可以添加普通变量和方法
private String name;
public void sleep(){
System.out.println("666");
}
//抽象类也是可以构造方法
public A(String name) {
this.name = name;
}
}
abstract class C extends A{
public int age;
public C(String name) {
super(name);
}
}
//或者
class E extends A{
public int age;
public E(String name) {
super(name);
}
@Override
public void eat() {
System.out.println("888");
}
}
- 抽象方法不能被final和private修饰,因为抽象方法要被子类进行重写(但其实就算子类是抽象类没有对父类的抽象方法进行重写,父类中的抽象方法也是不能被final和private修饰的)
- 抽象方法不能被static修饰,原因也是抽象方法要被子类进行重写,虽然被static修饰后事不依赖对象的,但是既然是抽象类,那么其中的抽象方法一定会被子类所重写,所以抽象方法是不能被static修饰的
- 抽象类中不一定包含有抽象方法,但是有抽象方法的类一定是抽象类
- 抽象类中可以有构造方法,供子类创建对象的时候,初始化父类的成员变量
- 抽象类存在的最大意义就是为了被继承
- 抽象类也可以发生向上转型,进一步发生多态
以上抽象类的这几条特性,只要读透、把握好,最后其实会发现抽象类也并不是很难理解。可以仔细理解下面这段示例代码:
abstract class Shape{
public abstract void draw();
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("◇");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
public class Main {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
drawMap(new Cycle());
drawMap(new Rect());
drawMap(new Triangle());
}
}
运行结果:
为什么会有抽象类这种东西?
从上面抽象类的那么多特性可以知道一点也是较为重要的一点:抽象类本身是不能被实例化的,如果想要使用它,就只能创建这个抽象类的子类,让这个子类重写抽象类中的抽象方法。
那么,这时候有些人就会有一个疑问:在之前学习中的普通类也是能够实现本文抽象类中这些功能(可以别继承,也可以被重写),那么为何还要这么麻烦再来学习抽象类这样的东西,还要考虑比普通类多这么些的特性呢?
对于这样的疑问,我的回答是:其实抽象类相当于多了一重编译器的校验。例如有些程序本来是不可以调用父类中的内容的,但是如果不小心调用了父类的内容,对于普通类时不会报错的,但是对于抽象类会直接报错,这样有助于快速定位到错误的地方。
接口
什么是接口?
在Java中,接口其实就是一种行为的规范和标准,可以看成:多个类的公共规范,是一种引用数据类型。
interface I{
public abstract void func1(); //默认就是public abstract,可以省略不写
void func2();
}
接口的定义格式与类的定义格式基本是相同的,只是将class关键字换成interface关键字而已。
注意:
- 在接口中的抽象方法中public abstract是固定搭配,在定义抽象方法的时候,可以省略不写
- 创建接口的时候,接口命名一般都是以大写字母 I 开头
接口的特性
- 子类和父类之间是extend继承关系,类与接口之间是implement实现关系
- 接口中每一个成员方法都是抽象方法,如果不写修饰符,则接口中的方法会被隐式指定为public abstract;如果写也只能是写public abstract,写成其他的修饰符都会直接报错
- 接口中的每一个成员变量都会被隐式制定为public static final变量
- 接口类型是一种引用类型,但是不能直接new接口对象,是不能够进行实例化的
- 接口中的方法是不能在接口中实现的,因为接口中的方法默认都是抽象方法,只能由实现接口的类来进行实现
- 接口中的方法,如果要实现,需要使用default来进行修饰
interface I{
//可以不在实现接口类中进行重写
default void func() {
System.out.println("666");
}
}
- 接口中的静态方法可以有具体的实现
interface I{
//和前面一样,是不依赖对象的
public static void func2(){
System.out.println("这是一个静态方法");
}
}
- 一个普通的类可以通过implements来实现这个接口
- 接口也可以发生向上转型,进一步发生多态
- 接口虽然不是类,但是接口编译完成后字节码文件后缀格式也是.class的文件夹
以上接口的这几条特性,跟抽象类一样,只要读透、把握好,最后其实会发现接口和前面的那些只是点并无两别。可以仔细理解下面这段示例代码:
interface IShape{
public abstract void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect implements IShape {
@Override
public void draw() {
System.out.println("◇");
}
}
class Triangle implements IShape {
@Override
public void draw() {
System.out.println("△");
}
}
public class Main {
public static void drawMap(IShape shape){
shape.draw();
}
public static void main(String[] args) {
drawMap(new Cycle());
drawMap(new Rect());
drawMap(new Triangle());
}
}
匿名内部类(拓展)
在前面“类和对象”的文章中内部类部分详细地讲解了实例内部类和静态内部类,而匿名内部类会涉及到接口部分相关的知识,所以放在本文此处进行讲解。
示例代码:
interface I{
default void func() {
System.out.println("666");
}
void func2();
}
public class Main {
public static void main(String[] args) {
I i=new I() {
//对于func(),是default修饰的,可以重写也可以不重写
@Override
public void func() {
System.out.println("111");
}
//对于func2(),因为是纯抽象方法,必须进行重写
@Override
public void func2() {
System.out.println("333");
}
};
i.func(); //111
i.func2(); //333
//匿名内部类只能够使用一次
I i2=new I() {
@Override
public void func2() {
}
};
i2.func(); //666
i2.func2(); //无打印值
}
}
从上面的代码中可以得出结论:
● 匿名内部类是在实例化接口的时候出现的,可以对接口中的方法进行重写(其中,接口中的纯抽象方法必须进行重写)
● 匿名内部类只能够使用一次,也就是在第二次调用这个接口的时候,还会是原来的效果
实现多个接口
在Java中,类和类之间是进行单继承的,一个类只能有一个父类,也就是说,在Java中是不支持多继承的。那么如何跟C++一样可以实现多继承呢?
因为在Java中是不支持多继承,所以引出了接口这个概念。虽然不能实现多继承,但是可以一个类实现多个接口。
示例代码:
class Animal{
public String name;
public int age;
public Animal(String name) {
this.name = name;
}
}
interface IRun{
void run();
}
interface ISwim{
void swim();
}
interface IFly{
void fly();
}
class Duck extends Animal implements IRun,ISwim,IFly{
public Duck(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name+"正在跑");
}
@Override
public void swim() {
System.out.println(this.name+"正在游");
}
@Override
public void fly() {
System.out.println(this.name+"正在飞");
}
}
public class Main {
public static void main(String[] args) {
Duck duck=new Duck("小灰");
duck.run();
duck.swim();
duck.fly();
}
}
运行结果:
接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间是可以多继承的。
类和接口之间的关系是implement,接口和接口之间的关系是extends。
示例代码:
interface IRun{
void run();
}
interface ISwim{
void swim();
}
interface IFly{
void fly();
}
interface IDuck extends IRun,ISwim,IFly{
}
class Duck implements IDuck{
@Override
public void run() {
}
@Override
public void swim() {
}
@Override
public void fly() {
}
}
总结:其实接口间的继承就相当于把多个接口合并在一起,拓展了之前的功能。但是最后在实现这个接口的时候,还是要对之前接口中的抽象方法进行重写。
三个重要的接口
Comparable接口
此接口可以对对象数组进行排序。在此之前,我们排序基本上都是对数组元素进行排序,但是针对对象进行排序,又应该如何实现呢?
就比如说,我有如下的对象数组,想要对这些对象按照年龄进行排序:
Student[] students=new Student[3];
students[0]=new Student("张三",18);
students[1]=new Student("李四",23);
students[2]=new Student("王五",9);
这时候,如果只是简单地对对象进行>或者<进行判断,又或者使用equals来进行判断都会是不行的,因为在代码中其实就并未指定到底是按何种方式进行排序的(姓名还是年龄)。
所以就引出了Comparable接口,使用Comparable接口来调用Student类,然后来对这个接口中排序的抽象方法进行重写(也就是重写Comparable接口中的compareTo方法),就可以达到按姓名或者年龄排序的效果了,示例代码:
//按年龄进行排序
class Student implements Comparable<Student>{
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return this.age-o.age;
}
}
public class Main {
public static void main(String[] args) {
Student[] students=new Student[3];
students[0]=new Student("张三",18);
students[1]=new Student("李四",23);
students[2]=new Student("王五",9);
System.out.println("排序前:"+ Arrays.toString(students));
Arrays.sort(students);
System.out.println("排序后:"+ Arrays.toString(students));
}
}
Comparator接口
在上面的Comparable接口,我们会发现一个与实际开发不符的地方:把实现排序的方法写在Student类中,这导致这个排序方法直接就写死了。就比如,我已经实现了按年龄排序,但是我有想再按姓名进行排序,那么这时候可能就要先将原本的按年龄排序方法先注释掉,再重写一个按姓名排序的方法,长此以往,效率实在是太低了。那么,又该如何解决这样的问题呢?
在Java中,这时候就引出了另外一个接口——Comparator接口,重写Comparator接口接口中的compareTo方法,使用这个接口来跟Comparable接口打配合,即可完成多种排序共同存在,想要什么排序就直接在sort方法中传入哪个类即可。
示例代码:
class Student implements Comparable<Student>{
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return 0;
}
}
//根据年龄比较
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age-o2.age;
}
}
//根据姓名比较
class NameComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public class TestDemo1 {
public static void main(String[] args) {
Student[] students=new Student[3];
students[0]=new Student("张三",18);
students[1]=new Student("李四",23);
students[2]=new Student("王五",9);
AgeComparator ageComparator=new AgeComparator();
System.out.println("排序前:"+ Arrays.toString(students));
Arrays.sort(students,ageComparator);
System.out.println("排序后:"+ Arrays.toString(students));
}
}
**注意:**sort方法之前我们都是只传了一个参数,其实它是可以传入第二个参数的,也就是可以传入一个比较器。
Clonable接口
我们在前面对数组进行拷贝的时候,用的是clone方法进行拷贝。但是如果我们是想对一个对象进行拷贝的话,又该如何操作呢?
很显然,直接调用clone方法肯定是不行的,这时候我们就要使用一个接口:Clonable接口,这样之后就可以合法调用Object类中的clone方法。
示例代码:
class A implements Cloneable{
public int a=10;
public int b=20;
@Override
public String toString() {
return "A{" +
"a=" + a +
", b=" + b +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A();
A b= (A) a.clone();
System.out.println(b);
}
}
注意:需要重写Object类中的clone方法;此处使用的throws是抛出异常(必须写上,否则会报错)只需了解即可,在后面文章中介绍到异常会详细讲到。
浅拷贝和深拷贝
浅拷贝
在上面例子中,我们实现的拷贝是对类中的基本数据类型进行拷贝。那么这时候就会有一个疑问,对于类中的非基本数据类型(比如String等引用类型变量),我们又应该如何进行拷贝呢?
如果我们还是像上面代码一样,就会出现一些问题,先上代码:
class B {
public int c=100;
}
class A implements Cloneable{
public int a=10;
public B b=new B();
@Override
public String toString() {
return "A{" +
"a=" + a +
", b=" + b +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A();
A b= (A) a.clone();
System.out.println(a.b.c);
System.out.println(b.b.c);
System.out.println("==============");
a.b.c=50;
System.out.println(a.b.c);
System.out.println(b.b.c);
}
}
运行结果:
从上面代码以及运行结果可以看出,当对对象a中调用的其他对象中的成员变量进行修改之后,对象b也会是相同的结果,也就是说这个代码其实没有实现真正的拷贝,因为b对象会随着a对象的改变而改变,其实b对象只是对a对象的一个拷贝,而b对象中b引用指向的还是原来a对象中b引用的对象,这句话可能会比较难理解,这里通过画图来进行展示:
对于这种只拷贝了一半,有一部分并没有进行拷贝,就叫做浅拷贝。
深拷贝
对于深拷贝来说,相较于浅拷贝来说,是对整个对象全部都进行拷贝的。就是说,会先将一个对象1进行拷贝得到对象2,再将对象1引用的对象3进行拷贝得到对象4,接着让对象2引用对象4,这样的话,如果再对对象1引用的对象3进行修改,也不会影响到对象2和对象4了。下面通过画图展示:
示例代码:
class B implements Cloneable{
public int c=100;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class A implements Cloneable{
public int a=10;
public B b=new B();
@Override
public String toString() {
return "A{" +
"a=" + a +
", b=" + b +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
A tmp=(A)super.clone();
tmp.b=(B)this.b.clone();
return tmp;
}
}
public class TestDemo1 {
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A();
A b= (A) a.clone();
System.out.println(a.b.c);
System.out.println(b.b.c);
System.out.println("==============");
a.b.c=50;
System.out.println(a.b.c);
System.out.println(b.b.c);
}
}
运行结果:
抽象类和接口的区别
在前面的学习中,我们已经知道了抽象类和接口的使用方法以及注意事项等,接下来就对两者的一个区分。
在Java中,抽象类和接口都是实现多态的常用方式,其核心区别是:抽象类可以包含普通方法和普通字段,这些普通方法和普通字段在子类中是可以直接进行使用的,不需要对其进行重写操作;而接口中不能包含普通方法,子类必须重写接口中的所有抽象方法。
区别 | 抽象类 | 接口 |
---|---|---|
结构组成 | 普通类+抽象方法 | 抽象方法+全局常量 |
权限 | 各种权限 | public |
子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
关系 | 一个抽象类可以实现若干接口 | 接口不能继承抽象类,但是接口可以使用extends关键字继承多个父接口 |
子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
Object类
在Java中,默认提供了一个类:Object类。Object类默认是所有类的父类,也就是说所有类的对象都可以使用Object的引用进行接收(就比如上面克隆对象讲到的)。
本文只是对Object类中的一部分方法进行简单的介绍。完整内容后续掌握。
对象打印toString方法
前面文章介绍过,详细见之前文章。
对象比较equals方法
在Java中,对==进行比较的时候:
- 如果==两边都是基本数据类型,比较的是变量之间的值是否相同
- 如果==两边是引用类型变量,比较的是引用变量之间的地址是否相同
- 如果要比较对象中的内容,必须重写Object类中的equals方法,因为equals方法默认是按照地址进行比较的
class A{
public int a=10;
public String b="abc";
}
public class Main {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
int a=1;
int b=1;
System.out.println(a==b); //true
System.out.println(a1==a2); //false 按地址进行比较
System.out.println(a1.equals(a2)); //false 按地址进行比较
}
}
重写equals方法之后:
class A{
public int a=10;
public String b="abc";
@Override
public boolean equals(Object o) {
if (this == o) return true; //两个引用 引用同一个对象
if (o == null || getClass() != o.getClass()) return false;
A a1 = (A) o;
return a == a1.a && Objects.equals(b, a1.b);
}
}
public class Main {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
int a=1;
int b=1;
System.out.println(a==b); //true
System.out.println(a1==a2); //false
System.out.println(a1.equals(a2)); //true
}
}
总结:在比较对象是否相同的时候,一定要重写equals方法。
hashcode方法
hashcode()方法是一个能够帮我们算具体对象位置存在的方法。这个方法是一个native方法,底层是由C/C++代码写的。
如果不对hashcode进行重写的话,因为a1和a2都是new出来的对象,那么即使这两个对象里面存储的内容一样,也会判定这两个对象是在不同位置的(地址是不相等的)。
class A{
public int a=10;
public String b="abc";
}
public class Main {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.hashCode());
System.out.println(a2.hashCode());
}
}
运行结果:
对hashcode进行重写后,因为new出来的这两个对象存储的内容是一样的,是由重写之后会判定这两个对象的地址是一样的。
class A{
public int a=10;
public String b="abc";
@Override
public int hashCode() {
return Objects.hash(a, b);
}
}
public class Main {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.hashCode());
System.out.println(a2.hashCode());
}
}
运行结果:
总结:
- hashcode方法用来确定对象在内存中存储的位置是否相同
- 事实上hashcode()在散列表中才有用,在其他情况下没用,在散列表中hashcode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置
接收引用数据类型
在前面我们知道了Object可以接收任意对象,因为Object是所有类的父类,但是Object并不局限于此,它可以接收所有数据类型,包括:类、数组、接口。
以接收接口为例:
interface I{
void func();
}
class A implements I{
@Override
public void func() {
System.out.println("666");
}
}
public class Main {
public static void main(String[] args) {
Object obj=new A(); //向上转型
A a=(A)obj; //向下转型
}
}
Object真正达到了参数统一,如果一个类希望接收所有的数据类型,就是Object完成,在Java中,泛型就是底层就是通过Object来实现的。