目录
1. 抽象类
1.1 抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。 比如:
//抽象类 abstract class Shape{ //抽象方法 public abstract void draw(); } public class Test { public static void main(String[] args) { } }
在这个Shape中的draw方法没有具体的实现,draw方法就为抽象方法,Shape就为抽象类
1.2 抽象类语法
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
// 抽象类:被abstract修饰的类 public abstract class Shape { // 抽象方法:被abstract修饰的方法,没有方法体 abstract public void draw(); abstract void calcArea(); // 抽象类也是类,也可以增加普通方法和属性 public double getArea(){ return area; } protected double area; // 面积 }
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法 ,抽象类最突出和普通类不一样的地方就是不可以进行实例化!!
1.3 抽象类特性
1.抽象类不能实例化
2.抽象类虽然不可以进行实例化,但是可以被继承【也就是说抽象类其实就是为了被继承】
//抽象类 abstract class Shape{ public int a; public static int b; public void func(){ } //抽象方法 public abstract void draw(); } class Ract extends Shape{ @Override public void draw() { System.out.println("画一个图形"); } } public class Test { public static void main(String[] args) { } }
3.当一个普通类继承这个抽象类之后,如果这个抽象类当中包含抽象方法,那么需要重写这个抽象方法,否则代码不能通过
4.当一个抽象类同时拥有多个抽象方法时,继承的这个类需要重写所有抽象方法
//抽象类 abstract class Shape{ public int a; public static int b; public void func(){ } //抽象方法 public abstract void draw();//1 public abstract void draw2();//2 } class Ract extends Shape{ @Override public void draw() { System.out.println("画一个图形"); } @Override public void draw2() { } } public class Test { public static void main(String[] args) { } }
5.如果抽象类A继承了抽象类B。那么抽象类A可以不重写B中的抽象方法
//抽象类 abstract class Shape{ public int a; public static int b; public void func(){ } //抽象方法 public abstract void draw();//1 public abstract void draw2();//2 } abstract class Ract extends Shape{ } public class Test { public static void main(String[] args) { } }
6.如果抽象类A继承了抽象类B。那么抽象类A可以不重写B中的抽象方法,但是当有一个类C继承了A方法后需要继承A,B中的所有抽象方法
//抽象类 abstract class B{ public int a; public static int b; public void func(){ } //抽象方法 public abstract void draw();//1 public abstract void draw2();//2 } abstract class A extends B { public abstract void draw3();//3 } class C extends A{ @Override public void draw() { } @Override public void draw2() { } @Override public void draw3() { } } public class DEmo { public static void main(String[] args) { } }
7.抽象方法不能是private的
abstract class Shape { abstract private void draw(); } // 编译出错 Error:(4, 27) java: 非法的修饰符组合: abstract和private
注意:抽象方法没有加访问限定符时,默认是public.
8. 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
9. 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
10. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
1.4 抽象类的作用
抽象类的作用,就是为了被继承,一是父类中的抽象方法在定义的时候可以不用写具体的执行体,二是编译器多了一层校验,当我们没有重写抽象方法的时候会直接报错。
2. 接口
2.1 接口的概念
接口就是对行为的规范,大家在实现的时候,只要按着规范,那就都可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型
就拿我们生活中的插座举例:
2.2 语法规则
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。
interface IShape{ public abstract void draw(); }
提示:
1,创建接口时,接口的名字一般以大写字母 I 开头。
2,接口的命名一般用形容词性的单词。
3,阿里编程规范中要求,接口中的方法和成员前不要加任何修饰符,保持代码的简洁,比如上面的public abstract,当然虽然我们没写,但是编译器时是知道的。
2.3 接口使用
接口是不能直接使用的,必须要有一个类来实现这个接口,并且重写接口里面所有的抽象方法才可
interface AnimalEat{ void eat(); } class Dog implements AnimalEat{ @Override public void eat() { System.out.println("狗狗吃狗粮!"); } } class Cat implements AnimalEat{ @Override public void eat() { System.out.println("猫猫吃猫粮!"); } } public class TestDemo { public static void main(String[] args) { AnimalEat animal1 = new Dog(); AnimalEat animal2 = new Cat(); animal1.eat(); animal2.eat(); } }
注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系
2.4 接口特性
1.接口当中的成员变量,默认都是 public static final 修饰的
2.接口当中的成员方法默认都是抽象方法:即接口中的方法会被隐式的指定为public abstract
3.接口当中的普通成员方法,是不能有具体的实现的
4.接口当中的普通成员方法,如果要有具体的实现,必须加上 default 修饰
5.接口当中可以有静态的成员方法,但是不管是静态的方法还是default修饰的方法,都是public修饰的
interface IShape{ int count = 10; void draw(); public default void func(){ } public static void func2(){ System.out.println("静态成员方法"); } }
public 是系统默认的可写可不写
6.接口也是不可以实例化的 ,接口类型是一种引用类型,但是不能直接new接口的对象
7.一个接口可以引用具体实现类,相当于是一个向上转型。
interface IShape{ void draw(); /*int count = 10;*/ /*default void func(){ } static void func2(){ System.out.println("静态成员方法"); }*/ } class Ract implements IShape{ @Override public void draw() { System.out.println("画一个矩形"); } } class Flower implements IShape{ @Override public void draw() { System.out.println("画一朵画"); } } public class Test { public static void drawMap(IShape shape){ shape.draw(); } public static void main(String[] args) { drawMap(new Flower()); drawMap(new Ract()); /* IShape iShape = new IShape();*/ } } //画一朵画 //画一个矩形 //
8.接口当中不能有静态,实例代码块,构造方法
public interface Shape { // 编译失败 public Shape(){ } {} // 编译失败 void draw(); void show(); }
9.接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
2.5 实现多个接口
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();
}
interface IFlying {//提供一个飞的接口
void fly();
}
interface IRunning {//提供一个跑的接口
void run();
}
interface ISwimming {//提供一个游泳的接口
void swim();
}
//叫 跑 游泳
class Duck extends Animal implements IRunning,IFlying,ISwimming{
public Duck(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name+"正在吃鸭粮");
}
@Override
public void fly() {
System.out.println(name+"正在飞");
}
@Override
public void run() {
System.out.println(name+"正在跑");
}
@Override
public void swim() {
System.out.println(name+"正在游泳");
}
}
class Dog extends Animal implements IRunning{
public Dog(String name,int age){
super(name,age);
}
@Override
public void eat() {
System.out.println(name+"正在吃狗粮");
}
@Override
public void run() {
System.out.println(name+"正在四条腿跑");
}
}
//叫 跑 飞
//一定是先继承再实现
class Bird extends Animal implements IRunning,IFlying{
public Bird(String name,int age){
super(name,age);
}
@Override
public void eat() {
System.out.println(name+"正在吃鸟粮");
}
@Override
public void run() {
System.out.println(name+"正在用两条腿跑");
}
@Override
public void fly() {
System.out.println(name+"正在用翅膀飞");
}
}
class Robot implements IFlying{
@Override
public void fly() {
System.out.println("机器人正在飞");
}
}
public class Test {
public static void walk(IRunning iRunning){
iRunning.run();
}
public static void fly(IFlying iFlying){
iFlying.fly();
}
public static void main(String[] args) {
//Dog没有实现FLY:fly(new Dog("狗子",1));
fly(new Bird("鹦鹉",1));//这两个类都实现了ifly接口
fly(new Duck("唐老鸭",1));
fly(new Robot());
}
public static void main2(String[] args) {
walk(new Dog("狗子",1));//这三个类都实现了running接口
walk(new Bird("鹦鹉",1));
walk(new Duck("唐老鸭",1));
}
public static void func1(Animal animal){
animal.eat();
}
public static void main1(String[] args) {
func1(new Dog("狗子",1));
func1(new Bird("鹦鹉",1));
func1(new Duck("唐老鸭",1));
}
}
注意:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .
猫是一种动物, 具有会跑的特性.
青蛙也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞
在这里就很好的映射了上面所说的 接口可以看成是:多个类的公共规范,是一种引用数据类型
我们在上面的代码中定义了 IRunning IFlying ISwimming 几个公共接口,所有的类都可以去调用它们,不用去管是不是Animal类型的,如Robot类可以调用IRunning,但是Robot并不是Animal类的子类。
在这个里面比如说游泳,这个时候肯定不能把游泳的方法定义在父类Animal里面了,因为Dog类也会继承到这个方法,但是狗不会游泳,这个时候就显得不是很恰当。那如果我们又多定义一个游泳的类,但是duck已经继承了Animal类,不可能再继承游泳类了。所以,接口这个时候就显得十分的好用,一个类可以实现多个接口,并且也可以实现多态的思想,对于各种各样不同的行为功能,我们只要在类的后面实现各个接口就好了。
2.6 接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到
多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.interface IA{ void funcA(); } interface IB{ void funcB(); } //接口的继承:拓展功能 interface IC extends IA,IB{//IC有IA和IB两个功能 void funcC(); } class T implements IC{ @Override public void funcA() { } @Override public void funcB() { } @Override public void funcC() { } } public class Test2 { public static void main(String[] args) { } }
接口间的继承相当于把多个接口合并在一起
2.7,几个重要接口使用实例
Comparable接口
public interface Comparable<T> { public int compareTo(T o); } //这是Comparable接口里面的方法情况,就一个comparableTo方法,在实现的类里面需要进行重写
假如这里有一个学生类,我们定义了一个学生数组,需要我们按照学生的年龄进行排序,那么如下实现:
class Student implements Comparable<Student>{ public String name; public int age; public double score; public Student(String name, int age, double score) { this.name = name; this.age = age; this.score = score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", score=" + score + '}'; } @Override public int compareTo(Student o) { return this.age-o.age; } } public class Test { public static void main(String[] args) { Student[] stu = new Student[3]; stu[0] = new Student("xiaowang",18,88.0); stu[1] = new Student("zhangsan",19,66.0); stu[2] = new Student("lisi",12,88.9); System.out.println("排序前" +Arrays.toString( stu)); Arrays.sort(stu); //比较两个对象的大小 //error: if (student>student2){} System.out.println("排序后" +Arrays.toString( stu)); } }
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力. 通
过重写 compareTo 方法的方式, 就可以定义比较规则这是我们在学生类里面重写了compareTo方法,按照两个学生对象的年龄进行比较。其中,this.age代表你调用这个方法的对象的年龄,比如上面的o.age代表你传参传入的对象的年龄。当我们对数组进行排序调用sort方法的时候,sort方法的内部会自动调用compareTo方法,在默认的逻辑上,当this.age - o.age < 0 的时候不会进行交换,所以排序出来也就是升序的;但如果我们交换一下变成 o.age - this.age的话,那么原本是升序的,比如3,5,这个时候你用5-3 > 0,那么大于0就需要进行交换,所以最后排序出的结果也就是降序的。
总结:如果以后是自定义类型的数据,牵扯到大小比较,需要进行一些设计的。比如实现接口。
但是在这里面我们已经写死了,如果我们想按照其他类型比较的话只能在 compareTo方法内区改变this和o,那这么搞的话会影响到其他代码的运行
Comparator接口
又称比较器
class Student implements Comparable<Student>{ public String name; public int age; public double score; public Student(String name, int age, double score) { this.name = name; this.age = age; this.score = score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", score=" + score + '}'; } @Override public int compareTo(Student o) { return this.age-o.age; } } //根据年纪比较 class AgeComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.age-o2.age; } } //根据分数比较 class ScoreComparator implements Comparator<Student>{ @Override public int compare(Student o1, Student o2) { return (int)(o1.score-o2.score); } } public class Test { public static void main(String[] args) { Student student1 = new Student("bit",10,78.9); Student student2 = new Student("abc",11,88.9); //比较年龄 AgeComparator ageComparator = new AgeComparator(); int ret = ageComparator.compare(student1,student2); System.out.println(ret); //比较分数 ScoreComparator scoreComparator = new ScoreComparator(); int ret2 = scoreComparator.compare(student1,student2); System.out.println(ret2); } }
可以根据自己的需要去比较大小
我们可以将不同类型的比较方法都分离出来,在需要的时候调用,不会影响到其他的代码的运行,这样就可以将我们具体的比较过程与我们的学生类分离开,就可以定义多个不同的比较类,多个比较依据,根据不同的比较类去定义不同的对象,然后将学生对象作为参数进行传入到compare方法里面,这样调用不同的比较类的比较方法,根据用户的需求,自己去调用比较器。
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)
public static void sort(Comparable[] array){//因为学生类实现了Comparable接口的,所以用来接收学生数组 for(int i = 0;i < array.length - 1;i++){ for(int j = 0;j < array.length - 1 - i;j++){ if(array[j].compareTo(array[j+1]) > 0){ Comparable tmp = array[j]; array[j] = array[j+1]; array[j+1] = tmp; } } } } ..... //在main函数里面就可以直接用sort(stu)就行,只不过这里的compareTo依旧是你在Student里面重写的那个,以年龄为依据比较
2.8Clonable 接口和深拷贝
Object类里面有一个clone方法,可以用来创建一个对象的拷贝,但是如果想要使用clone方法,必须先实现Clonable接口,不然就会抛出一个CloneNotSupportedException 异常。
可以看到,我们的Clonable接口里面实际上是没有任何抽象方法的,这叫做空接口或者标记接口,表示该类可以进行克隆
class Person implements Cloneable{ public int age; @Override protected Object clone() throws CloneNotSupportedException { return super.clone();//要克隆必须要重写一个克隆方法 } @Override public String toString() { return "person{" + "age=" + age + '}'; } } public class Test { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(); Person person1 = (Person) person.clone(); } }
浅拷贝 VS 深拷贝
浅拷贝
class Money{ public double money = 19.9; } class Person implements Cloneable{ public int age = 10; public Money m = new Money(); @Override protected Object clone() throws CloneNotSupportedException { return super.clone();//要克隆必须要重写一个克隆方法 } @Override public String toString() { return "person{" + "age=" + age + '}'; } } public class Test { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(); Person person2 = (Person) person.clone(); System.out.println(person.m.money); System.out.println(person2.m.money); System.out.println("========================="); person2.m.money = 99.9; System.out.println(person.m.money); System.out.println(person2.m.money); } }
浅拷贝是只拷贝了这个对象,没有拷贝对象里面的引用所指的对象
如上代码,我们可以看到,通过clone,我们只是拷贝了Person对象。但是Person对象中的Money对象,并没有拷贝。通过person2这个引用修改了m的值后,person这个引用访问m的时候,值也发生了改变。这里就是发生了浅拷贝
所以在这个代码中我们一旦更改了money的值拷贝的和被拷贝的值都发生了变化
深拷贝
class Money implements Cloneable{ public double money = 19.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone();//重写克隆方法 } } class Person implements Cloneable{ public int age = 10; public Money m = new Money(); @Override protected Object clone() throws CloneNotSupportedException { Person temp = (Person)super.clone(); temp.m = (Money)this.m.clone(); return temp; //return super.clone();//要克隆必须要重写一个克隆方法 } @Override public String toString() { return "person{" + "age=" + age + '}'; } } public class Test { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(); Person person2 = (Person) person.clone(); System.out.println(person.m.money); System.out.println(person2.m.money); System.out.println("========================="); person2.m.money = 99.9; System.out.println(person.m.money); System.out.println(person2.m.money); } }
我们需要让Money类也实现Cloneable这个接口,后重写克隆方法这时我们就可以去实现两个克隆,对数据有更深层的克隆,我们就可以对Money也进行克隆了,我们可以看到在Person的克隆方法中我们先定义一个临时的变量temp,将Person和Money都克隆给他,后返回temp给person2,就完成了深拷贝
对于一个拷贝到底是深拷贝还是浅拷贝,不取决于数据的类型,完全是取决于代码的实现
2.9 抽象类和接口的区别
3. Object类
Object类是Java默认提供的一个类,它是所有类的父类,可与称之为超类。
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父
类。即所有类的对象都可以使用Object的引用进行接收。class Person{} class Student{} public class Test { public static void main(String[] args) { function(new Person()); function(new Student()); } public static void function(Object obj) { System.out.println(obj); } } //执行结果: Person@1b6d3586 Student@4554617c
可以用Object接收任意类的对象
3.1,Object类方法总览
对于整个Object类中的方法需要实现全部掌握。
本小节当中,我们主要来熟悉这几个方法:toString()方法,equals()方法,hashcode()方法
3.1.1 获取对象信息 :toString()方法
果要打印对象中的内容,可以直接重写Object类中的toString()方法
// Object类中的toString()方法实现: public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
3.1.2 对象比较:equals方法
在Java中,==进行比较时有三种情况:
a.如果==左右两侧是基本类型变量,比较的是变量中值是否相同
b.如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同class Person{ public String id; public Person(String id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + id + '\'' + '}'; } } class Student{ } public class Test { public static void func(Object obj) { System.out.println(obj); } public static void main(String[] args) { Person person1 = new Person("123"); Person person2 = new Person("123"); System.out.println(person1==person2); } }
c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:
equals底层代码:
class Person{ public String id; public Person(String id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + id + '\'' + '}'; } @Override public boolean equals(Object obj){ //判断obj参数是否为空 if (obj == null){ return false; } if(this == obj){ return true;//有可能是对象之间的赋值,例如per2 == per1,那两个肯定是相等的 } if(!(obj instanceof Person)){ return false; //两个毫不相关的类的属性不能进行比较 //obj根本就不是Person类的对象,所以肯定不相等 } Person tmp = (Person) obj; return this.id.equals(tmp.id);//Sting类型的equals } } class Student{ } public class Test { public static void func(Object obj) { System.out.println(obj); } public static void main(String[] args) { Person person1 = new Person("123"); Person person2 = new Person("123"); System.out.println(person1==person2); //System.out.println("equals未重写的 "+person1.equals(person2)); System.out.println("equals重写后的 "+person1.equals(person2)); } }
自定义类型都需要重写equals的!!
equals:判断对象是否相同
3.1.2 hashcode
hashcode方法的作用是帮助我们定位一个对象的具体的位置。
hashcode方法源码:
public native int hashCode();
我们认为两个id一样的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例
代码:class Person{ public String id; public Person(String id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + id + '\'' + '}'; } } class Student{ } public class Test { public static void func(Object obj) { System.out.println(obj); } public static void main(String[] args) { Person person1 = new Person("123"); Person person2 = new Person("123"); System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); } }
注意事项:两个对象的hash值是不一样的
在未重写hashCode时,id相同时hash值是不一样的
class Person{ public String id; public Person(String id) { this.id = id; } } @Override public int hashCode(){ return Objects.hash(id); } } public class Test { public static void func(Object obj) { System.out.println(obj); } public static void main(String[] args) { Person person1 = new Person("123"); Person person2 = new Person("123"); //将来对于他们我们想放在同一个位置上 System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); } }
在重写hashCode后两个地址就一样了
其实一般情况下,对于自定义的类型,上面的equals和hashcode方法最好都是需要重写下的,因为你不清楚什么时候可能就会用到,而编译器也是为我们提供了自动生成的快捷方式。
然后一轮next就会得到
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return Objects.equals(id, person.id); } @Override public int hashCode() { return Objects.hash(id); }