文章目录
抽象类
什么是抽象类?
常见的,如果我们在类中写了一个方法,但是这个方法并没有具体的实现细节(不给出具体的实现体),那么我们就可以将这个方法设计成一个抽象方法,那么包含抽象方法的类就可以称为抽象类。
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());
}
}
实现多个接口
因为在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();
}
}
运行结果:
接口间的继承
类和接口之间的关系是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接口
在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方法肯定是不行的,这时候我们就要使用一个接口: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是抛出异常(必须写上,否则会报错)只需了解即可,在后面文章中介绍到异常会详细讲到。
浅拷贝和深拷贝
浅拷贝
如果我们还是像上面代码一样,就会出现一些问题,先上代码:
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类
本文只是对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进行重写的话,因为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()的作用是获取对象的散列码,进而确定该对象在散列表中的位置
接收引用数据类型
以接收接口为例:
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来实现的。