目录
1. 抽象类
1.1 抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是,并不是所有的类都是用来描绘对象的。
如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类就被称为抽象类。 比如:
分析说明:
- Animal是动物类,每个动物都有叫的方法,但由于Animal不是一个具体的动物,因此其内部bark()方法无法具体实现。
- Dog是狗类,首先狗属于动物,因此与Animal是继承关系,其次狗是一种具体的动物,“汪汪汪”叫,则bark()方法可实现。
- Cat是猫类,首先猫是动物,因此与Animal是继承关系,其次猫是一种具体的动物,“喵喵喵“叫,其bark()可以实现。
- 结合上面抽象类的概念发现:Animal可以设计为”抽象类“。
代码如下:
class Animal {
public void bark() {
System.out.println("喊叫....");
}
}
class Dog extends Animal {
@Override
public void bark() {
System.out.println("正在旺旺叫.....");
}
}
class Cat extends Animal {
@Override
public void bark() {
System.out.println("正在喵喵叫.....");
}
}
public class test1 {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.bark();
animal2.bark();
}
}
我们可以发现父类Animal的bark方法并没有实际工作,主要的都是由子类中的bark方法实现的。
对于这种没有实际工作的方法,我们可以设计成一个抽象方法(abstract method),而包含抽象方法的类我们称之为抽象类(abstract class)。
1.2 抽象类的语法
在Java中,一个类如果被一个abstract修饰则称为抽象类。抽象类中被abstract修饰的方法被称为抽象方法。抽象方法不用给出具体的实现体。
根据以上知识进行代码优化:
//修改前:
class Animal{
public void bark(){
System.out.println("喊叫....");
}
}
//修改后:
abstract class Animal{
public abstract void bark();
//被abstract修饰的类没有实现体
}
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法。
1.3 抽象类的特性
抽象类不能直接实例化对象。
public class test2 {
public static void main(String[] args) {
Animal animal = new Animal("1",1);
animal.bark();
}
}
但前面我们提到过抽象类也是类,可以存在构造方法。 这里说不能直接实例化对象,重点在于这个“直接”。
抽象类当中可以有构造方法,但是这个构造方法并不是在实例化这个抽象类的时候使用,因为他不能被实例化。实际上是子类继承该构造方法,通过子类调用在子类中间接对该抽象类进行实例化。
- 抽象方法是不能是private的。会出现编译出错: Error:(4, 27) java: 非法的修饰符组合: abstract和private
- 抽象方法不能被final和protected修饰。会出现编译出错: Error:(20, 25) java: 非法的修饰符组合: abstract和final Error:(21, 33) java: 非法的修饰符组合: abstract和static
- 抽象类必须被继承,并且继承后子类要重写父类中的所有抽象方法,否则子类也是抽象方法,必须要使用abstract修饰
1.4 抽象类的作用
抽象类本身不能被实例化,要想使用,只能常见该抽象类的子类,然后让子类重写抽象类中的抽象方法。
但实际感受下来会发现:明明普通的类也可以被重写,也可以被继承,那为什么要有抽象类和抽象方法这么个东西呢?
确实如此,但使用抽象类修饰相当于多了一层编译器的校验。
其实很多语法存在的意义就是为了”预防出错“,曾经谈到的final关键字也是类似,创建的变量用户不去修改,就等同于常量,但加上final之后能在后续代码不小心误修改的时候,让编译器及时提醒我们。充分利用编译器的校验,在实际开发中是很有意义的。
2. Object类
2.1 Object类的概念
Object是Java默认提供的一个类。Java中除了Object类,所有的类都是存在继承关系的。 默认会继承Object类。 即所有类的对象都可以使用Object的引用进行接收。
(会有一种Object类是所有类的祖先/始祖的感觉)
举个栗子:用Object类接收其他类的对象
class Student{}
class Teacher{}
public class test {
public static void func(Object obj) { //参数为Object类
System.out.println(obj);
}
public static void main(String[] args) {
Student stu = new Student();
Teacher tea = new Teacher();
func(stu);
func(tea);
}
}
运行结果:
Student@1b6d3586
Teacher@4554617c
所以在开发之中,Object类是参数的最高统一类型,同时该类还具有一些已经定义好的方法:
通过IDEA双击shift来实现查找:
点击后左侧或快捷键shift+7来查看类中方法:
接下来主要来了解以下三个方法:toString()方法、equals()方法和hashcode()方法。
2.2 获取对象信息toString方法
根据查找我们可以看到定义的toString()方法:
Object类中的toString方法返回值是一个字符串:类名+“@”+该对象的哈希码的无符号十六进制表示组成,但我们一般使用时返回的都是直观简洁的以文本信息表示的字符串,建议子类使用时都重写此方法。
2.3 对象比较equals方法
查找equals方法具体代码如下:
可以发现返回值中有==
判断,那么我们需要先对该符号在Java中的使用有所了解。
在Java中,== 会返回Boolean值,进行比较时:
- 如果 == 左右两侧是基本类型变量,比较的是变量中的值是否相同
- 如果 == 左右两侧是引用类型变量,比较的是引用变量地址是否相同
由此我们可以知道,如果要比较对象中内容,必须要重写Object类中的equals方法,因为equals方法中的返回内容也是按照地址进行比较的(如上图:return(this == obj))。
package demo1;
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class test1 {
public static void main(String[] args) {
Person p1 = new Person("owo",18);
Person p2 = new Person("omo",2);
int a = 1;
int b = 1;
System.out.println(a == b); // 基本类型变量比较
System.out.println(p1 == p2); // 引用类型变量比较
System.out.println(p1.equals(p2)); // equals方法比较
}
}
编译结果:
true
false
false
Person类重写equals方法后,然后比较:
package demo1;
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if(obj == null){
return false;
}
if(!(obj instanceof Person))
return false;
}
if(this == obj) {
return true;
}
// 比较属性值,使用向下转型
Person person = (Person)obj;
return this.name.equals(person.name) && this.age == person.age;
}
}
public class test1 {
public static void main(String[] args) {
Person p1 = new Person("owo",18);
Person p2 = new Person("omo",18);
int a = 1;
int b = 1;
System.out.println(a == b);//基本类型变量比较
System.out.println(p1 == p2);//引用类型变量比较
System.out.println(p1.equals(p2));//equals方法比较
}
}
2.4 获取位置hashcode方法
package demo1;
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if(obj == null){
return false;
}
if(!(obj instanceof Person))
return false;
}
if(this == obj) {
return true;
}
// 比较属性值,使用向下转型
Person person = (Person)obj;
return this.name.equals(person.name) && this.age == person.age;
}
}
public class test1 {
public static void main(String[] args) {
Person p1 = new Person("owo",18);
Person p2 = new Person("omo",18);
int a = 1;
int b = 1;
System.out.println(a == b);//基本类型变量比较
System.out.println(p1 == p2);//引用类型变量比较
System.out.println(p1.equals(p2));//equals方法比较
}
}
我们先来回忆刚刚的toString方法:
我们可以看到有hashcode方法。该方法作用是帮我算了一个具体的对象位置,确定对象在内存中存储的位置是否相同,这里面涉及数据结构,暂时没法讲述,理解为一个内存地址(暂时),然后调用integer.toHexString方法,将这个地址以十六进制输出。
hashcode源码:
这是个native 方法,底层是由C/C++代码写的,我们看不到。
我们认为两个名字相同年龄相同的人,应存储在同一位置。那么在Java环境中如何呢?我们看一段代码:
package demo1;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if(obj == null){
return false;
}
if(!(obj instanceof Person)) {
return false;
}
if(this == obj){
return true;
}
// 比较属性值,使用向下转型
Person person = (Person)obj;
return this.name.equals(person.name) && this.age == person.age;
}
}
public class test1 {
public static void main(String[] args) {
Person p1 = new Person("owo",18);
Person p2 = new Person("owo",18);
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
}
}
可以看到:两个对象的hash值不一样。
同样我们也可以重写hashcode()方法:
package demo1;
import java.util.Objects;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name,age);
}
}
public class test1 {
public static void main(String[] args) {
Person p1 = new Person("owo",18);
Person p2 = new Person("owo",18);
int a = 1;
int b = 1;
System.out.println(a == b); // 基本类型变量比较
System.out.println(p1 == p2); // 引用类型变量比较
System.out.println(p1.equals(p2)); // equals方法比较
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
}
}
现在输出的哈希值就相同了。
3. 内部类
3.1 内部类的概念
当一个事物的内部,有部分还需要一个完整的结构来描述,而这个内部的结构又只为外部结构所运用,此时这个内部的完整结构最好使用内部类。在Java中,类可以在类中定义,也可以在方法中定义,前者称为内部类,后者称为外部类。 内部类也是封装的一种体现。
public class OuterClass { ⬅外部类
class InnerClass { ⬅内部类
}
}
注意事项:
- 定义在class花括号{}外部的类,即使是在一个文件里,也不算是内部类。
- 内部类和外部类共用同一个Java类源文件,但是编译之后会形成单独的字节码文件。
代码如下:
编译后多了下面框起来的内部类字节码文件:
3.2 内部类的种类
据在外部类内部定义位置不同与修饰符不同可以大致分为以下几类:
- 成员内部类:普通内部类(没有任何修饰),静态内部类(static修饰)。
- 局部内部类(在外部类成员方法中定义)、匿名内部类。
内部类在我们日常使用中并不是非常常用,我们有时在看一些库的代码时可能会遇到,日常开始使用的最多的是匿名内部类。
3.2.1 静态内部类
定义:被static修饰的内部类叫静态内部类。
性质:
- 在静态内部类中只能访问外部类的静态成员。如果想要在内部类中访问,需要在内部类中实例化一个外部类对象,之后可以通过引用实现变量访问。
- 创建内部类对象时,不需要先创建外部类对象。
- 在外部类方法外实例化内部类时类的形式为外部类.内部类 类名 = new 外部类.内部类();
- 在外部类中实例化内部类对象时不需要加外部类.前缀来实例化。
样例代码:
package demo2;
class OuterClass {
int a = 100;
static int b = 100;
public void methodA() {}
public static void methodB() {}
// 被static修饰的内部类,静态内部类
public static class InnerClass {
public void methodInner() {
a = 10; // 编译失败,因为a不是静态外部类成员变量
b = 20;
methodA(); // 编译失败,因为该方法不是静态外部类成员方法
methodB();
}
}
}
public class test2 {
public static void main(String[] args) {
// 静态内部类对象创建和访问
OuterClass. InnerClass t2 = new OuterClass.InnerClass();
t2.methodInner();
}
}
3.2.2 实例化内部类
定义:不带static的普通内部类。
性质:
- 外部类的任何成员都能在内部类的成员方法中直接访问。
- 外部类不能直接访问内部类成员,要先对内部类进行实例化。
- 创建实例内部类对象时前提要先对外部类进行实例化创建。
- 在实例内部类方法中访问同名成员时,优先访问自己的,没有则访问外部类中成员(如想访问外部类成员需在对应对象前加外部类类名.this.同名成员)。
- 因该内部类和外部类处同一位置,其也受public、private限制。
public class OutClass {
private int a;
static int b;
int c;
public void methodA() {
a = 10;
System.out.println(a);
}
public static void methodB() {
System.out.println(b);
}
// 实例内部类:未被static修饰
class InnerClass {
int c;
public void methodInner() {
// 在实例内部类中可以直接访问外部类中:任意访问限定符修饰的成员
a = 100;
b = 200;
methodA();
methodB();
// 如果外部类和实例内部类中具有相同名称成员时,优先访问的是内部类自己的
c = 300;
System.out.println(c);
// 如果要访问外部类同名成员时候,必须:外部类名称.this.同名成员名字
OutClass.this.c = 400;
System.out.println(OutClass.this.c);
}
}
public static void main(String[] args) {
// 外部类:对象创建 以及 成员访问
OutClass outClass = new OutClass();
System.out.println(outClass.a);
System.out.println(OutClass.b);
System.out.println(outClass.c);
outClass.methodA();
outClass.methodB();
System.out.println("=============实例内部类的访问=============");
// 要访问实例内部类中成员,必须要创建实例内部类的对象
// 而普通内部类定义与外部类成员定义位置相同,因此创建实例内部类对象时必须借助外部类
// 创建实例内部类对象
OutClass.InnerClass innerClass1 = new OutClass().new InnerClass();
// 上述语法比较怪异,也可以先将外部类对象先创建出来,然后再创建实例内部类对象
OutClass.InnerClass innerClass2 = outClass.new InnerClass();
innerClass2.methodInner();
}
}
3.2.3 匿名内部类
定义:没有显式定义类名,而是在创建对象时通过传递实现了某个接口或继承了某个类的代码块的类。(有时使用匿名内部类可以无需创建新的类,减少代码长度 )
在实际开发中,实现一个类或接口的方法在程序中也许只会使用一次,为了这一次的使用在特地去创建一个实现的类,太过麻烦,此时我们就可以匿名内部类。
interface Interface01 {
void method();
}
class Hello implements Interface01 { // 专门创建的类
@Override
public void method() {
System.out.println("Hello!");
}
}
使用匿名内部类:
public class niming {
public static void main(String[] args) {
Interface01 interface01 = new Interface01() { // 这就是匿名内部类
@Override
public void method() {
System.out.println("niming::Hello!");
}
};
interface01.method();
}
}
框选部分即为匿名内部类
上面匿名内部类还有一种实现格式:
public static void main(String[] args) {
new Interface01() { // 这就是匿名内部类
@Override
public void method() {
System.out.println("niming::Hello!");
}
}.method(); // 直接进行调用
}
能实现同样的效果。
3.2.4 局部内部类
定义:定义在外部类成员方法里的内部类,该种内部类只能在其定义的位置中使用。
一般用的非常少,简单了解格式即可。
性质:
- 局部内部类只能在所定义的方法内部使用。
- 不能被public、static等修饰符修饰。
- 编译器对其也有自己的的字节码文件命名格式:外部类名字$ 数字 内部类名字.class
本篇完
下一篇是接口的知识点。