--- title: Java面向对象高级 date: 2020-10-06 14:51:51 tags: - Java - 面向对象 categories: - Java基础 --- ## 抽象类 ### 概念 有时候我们需要这样一个类,它不需要被实例化,也不需要实现完整的方法,它只是用来被继承的,用来限制子类的一个“规范”。这样的类我们就可以将它定义为**抽象类**,抽象类内的方法可以定义为**抽象方法**。 ```java 抽象类必须使用abstract class声明 一个抽象类中可以没有抽象方法。抽象方法必须写在抽象类或者接口中 只声明而未实现的方法称为抽象方法(未实现是指:没有“{}“方法体),抽象方法必须使用abstract关键字声明。 格式: abstract class 类名{ //抽象类 public abstract 返回值类型 方法名(); //抽象方法,只声明而未实现 } ``` > **注意:** > > 在抽象类的使用中有几个原则: > > * 抽象类本身是不能直接进行实例化操作的,即:不能直接使用关键字new完成。 > * 一个抽象类必须被子类所继承,被继承的子类(如果不是抽象类)则**必须覆写(重写)**抽象类中的全部抽象方法。 > > 常见的问题: > > * 抽象类不能使用final声明,因为final修饰的类是不能有子类的 ,而抽象类必须有子类才有意义。 > * 抽象类能有构造方法,而且子类对象实例化的时候的流程与普通类的继承是一样的,都是要先调用父类中的构造方法(默 认是无参的),之后再调用子类自己的构造方法。 ### 抽象类与普通类的区别 1. 抽象类必须用public或protected修饰(如果为private修饰,那么子类则无法继承,也就无法实现其抽象方法)。默认缺省为 public 2. 抽象类不可以使用new关键字创建对象,但是在子类创建对象时,抽象父类也会被JVM实例化。 3. 如果一个子类继承抽象类,那么必须实现其所有的抽象方法。如果有未实现的抽象方法,那么子类也必须定义为abstract类 ```java //Person.java 抽象类 public abstract class Person { public Person(){ //抽象类可以有构造方法 System.out.println("抽象类的构造方法执行了"); } public abstract void run(); //抽象方法 } //Student.java 子类 class Student extends Person { @Override public void run() { System.out.println("Student.run"); } } //Application.java 测试类 public class Application { public static void main(String[] args) { //可以定义Person对象变量,但它只能引用非抽象子类的对象 Person p = new Student(); //Student对象 父类的引用指向子类 p.run(); } } ``` ```tex 以上代码的运行结果为: 抽象类的构造方法执行了 Student.run 说明抽象类可以有构造方法,在new一个抽象类的非子类对象时,JVM会默认先执行抽象 类的构造方法。虽然我们不能new抽象类对象,但我们可以通过子类来操作抽象类中的资源。 ``` ## 接口 ### 接口的概念及定义 如果一个类中的全部方法都是抽象方法,全部属性都是全局常量,那么此时就可以将这个类定义成一个接 口。 定义格式: ```java interface 接口名称{ 全局常量 ; 抽象方法 ; } ``` > **面向接口编程思想** > > 接口是定义(规范,约束)与实现(名实分离的原则)的分离的思想。 > > 优点: > > 1. 降低程序的耦合性 > 2. 易于程序的扩展 > 3. 有利于程序的维护 ```java 因为接口本身都是由全局常量和抽象方法组成,所以接口中的成员定义可以简写: 1.全局常量编写时,可以省略 public static final 关键字,例如: public static final String INFO = "内容" ; 简写后: String INFO = "内容" ; 2.抽象方法编写时,可以省略 public abstract 关键字,例如: public abstract void print() ; 简写后: void print() ; ``` ### 接口的实现 implements 接口可以多实现,格式: ```java class 子类 implements 父接口1,父接口2...{ } ``` 以上的代码称为接口的实现。那么如果一个类即要实现接口,又要继承抽象类的话,则按照以下的格式编写 即可: ```java class 子类 extends 父类 implements 父接口1,父接口2...{ } ``` > 如果一个接口要想使用,必须依靠子类。 子类(如果不是抽象类的话)要实现接口中的所有抽象方法。 ```java //Person.java 接口 public interface Person { //int AGE = 10; //简写全局常量 void say(); //简写抽象方法 String getName(); } //Student.java public class Student implements Person{ //实现接口 private String name; public Student(){} public Student(String name){ this.name = name; } @Override public void say() { System.out.println(getName() + "say"); } @Override public String getName() { return this.name; } } //Application.java 测试类 public class Application { public static void main(String[] args) { Person p = new Student(); //Student对象 父类的引用指向子类 p.run(); } } ``` ### 接口的继承 接口因为都是抽象部分, 不存在具体的实现, 所以允许多继承,例如: ```java interface C extends A,B{ } ``` 继承相当于扩展了接口的方法 ### default方法 JDK1.8之后规定,在接口中,可以定义default方法。例如,把Person中的say()方法改为default方法 ```java //Person.java 接口 public interface Person { String getName(); //简写抽象方法 default void say(){ System.out.println(getName() + "say"); } } //Student.java public class Student implements Person{ //实现接口 private String name; public Student(){} public Student(String name){ this.name = name; } @Override public String getName() { return this.name; } } //Application.java 测试类 public class Application { public static void main(String[] args) { Person p = new Student("Li Ming"); //Student对象 父类的引用指向子类 p.run(); } } ``` 实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。 ### 接口与抽象类的区别 1. 抽象类要被子类继承,接口要被类实现。 2. 接口只能声明抽象方法,抽象类中可以声明抽象方法,也可以写非抽象方法。 3. 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。 4. 抽象类使用继承来使用, 无法多继承。 接口使用实现来使用, 可以多实现 5. 抽象类中可以包含static方法 ,但是接口中不允许(静态方法不能被子类重写,因此接口中不能声明 静态方法) 6. 接口不能有构造方法,但是抽象类可以有 ## Object 类 ### 概念 Object类是所有类的父类(基类),如果一个类没有明确的继承某一个具体的类,则将默认继承Object类。例如我们定义一个类: ```java public class Person{ } ``` 其实它被使用时 是这样的: ```java public class Person extends Object{ } ``` > **使用Object可以接收任意的引用数据类型** ### Object类中常用的方法 #### toString 建议重写Object中的toString方法。 此方法的作用:返回对象的字符串表示形式。 Object的toString方法, 返回对象的内存地址 类`Object`的`toString`方法返回一个字符串,该字符串由对象为实例的类的名称,字符“ `@` ”以及对象的哈希码的无符号十六进制表示形式组成。 换句话说,此方法返回一个等于值的字符串: ```java getClass().getName() + '@' + Integer.toHexString(hashCode()) ``` ```java //Person.java package top.oop.demo09; public class Person { private String name; private int age; private int id; //唯一标识符 public Person(String name, int age,int id) { this.name = name; this.age = age; this.id = id; } public Person() {} // getter和setter方法省略,若想放入ide中测试,请自行添加 } //Application.java 测试类 package top.oop.demo09; public class Application { public static void main(String[] args) { Person p = new Person("Li Ming",18); System.out.println(p.toString()); //top.oop.demo09.Person@27f674d } } ``` 可以看出,如果不重写toString(),那打印的结果明显不能体现出我们所创建的对象的特征。所以建议在类中去重写toString()方法。 我们可以在Person中重写toString()方法。 ```java @Override public String toString(){ return ("这是一个人,他叫" + this.name + "," + this.age + "岁了"); } ``` #### equals 先来看一段代码 ```java //Person.java 看上面toString中的Person.java //Application.java 测试类 package top.oop.demo09; public class Application { public static void main(String[] args) { Person p1 = new Person("Li Ming",18); Person p2 = new Person("Li Ming",18); System.out.println(p1 == p2); //false } } ``` 我们可以看出p1和p2的内容信息是完全一样的,但我们用 `== `比较时,会返回false。其实这也很好理解,每当我们new一个对象时就会在内存中开辟一块空间,也就是说p1和p2指向的是不同的内存地址,程序当然会判定他们不等。 但我们总需要对对象的信息进行比较,这时我们可以重写Object中的equals()方法来解决。不能直接用,直接用相当于还是在用`==` 比较。可以看Object中equals方法的源码如下: ```java public boolean equals(Object obj) { return (this == obj); } ``` 建议重写Object中的equals(Object obj)方法,此方法的作用:指示某个其他对象是否“等于”此对象。 Object的equals方法:实现了对象上最具区别的可能等价关系; 也就是说,对于任何**非空引用**值x和y,当且仅当 x和y引用同一对象( x == y具有值true )时,此方法返回true 。 > equals方法重写时的五个特性: > > * 自反性 :对于任何非空的参考值x , x.equals(x)应该返回true 。 > * 对称性 :对于任何非空引用值x和y,x.equals(y)应该返回true当且仅当y.equals(x)回报true 。 > * 传递性 :对于任何非空引用值x ,y和z ,如果x.equals(y)返回true且y.equals(z)返回true ,那么 x.equals(z)应该返回true 。 > * 一致性 :对于任何非空引用值x和y ,多次调用x.equals(y)始终返回true或始终返回false ,前提是未修改对象上的equals比较中使用的信息。 > * 非空性 :对于任何非空的参考值x , x.equals(null)应该返回false 。 equals一般根据我们的业务进行重写,举个例子,我们可以在Person类中重写equals方法,当唯一标识符id相等时,我们就认为这两个对象相等 ```java @Override public boolean equals(Object obj) { if(this == obj) //如果传入的对象与当前对象内存地址一样,那一定是相同的 return true; } if(obj == null){ //如果传入的对象为空,根据非空性,返回false return false; } if(o instanceof Person){ //如果传入的对象与当前对像类型相同 Person p2 = (Person)obj; //将obj对象强转为Person对象 if(this.id == p2.id){ return true; }else{ return false; } }else{ return false; } } ``` 以上代码可以进行简化,如下: ```java @Override public boolean equals(Object obj) { //如果传入的对象与当前对象内存地址一样,那一定是相同的 if(this == obj){ return true; } //如果传入的对象为空或与当前对象类型不同,返回false if(obj == null || !(obj instanceof Person){ return false; } Person p = (Person)obj; //将obj对象强转为Person对象 return (this.id == p.id); } ``` > IDEA 快捷键 `Alt+Inster `可以自动重写equals方法和toString方法 ## 内部类 在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。 广泛意义上的内部类一般来说包括这四种: 1. 成员内部类 2. 局部内部类 3. 匿名内部类 4. 静态内部类 ###成员内部类 **定义:**成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式: ```java class Outer { private double x = 0; public Outer(double x) { this.x = x; } class Inner { //成员内部类 public void say() { private double x = 200; //与外部类同名的变量 System.out.println("x="+ x); // x = 200.0 System.out.println("x=" + Outer.this.x); //访问外部类的同名成员 } } } ``` **特点:** 成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问: ```java 外部类.this.成员变量 外部类.this.成员方法 ``` **成员内部类的使用:** 以上面定义的Outer类为例,来看如何让使用它里面的内部类 ```java public class TestInner { public static void main(String[] args) { Outer outer = new Outer(100); Outer.Inner inner = outer.new Inner(); inner.say(); /*结果: * x = 200.0 * x = 100.0 */ } } ``` ###局部内部类 局部内部类是**定义在一个方法或者一个作用域里面的类**,它和成员内部类的区别在于**局部内部类的访问仅限** **于方法内或者该作用域内**。例如: ```java class Person{ public Person() { } } class Man{ public Man(){ } public People getPerson(){ class Student extends People{ //局部内部类 定义在了getPerson方法内 int age =0; } return new Student(); } } ``` > **注意:**局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及 > static修饰符的。 下面我们使用系统的某个API,来演示局部内部类的使用。 ```java import java.awt.Frame; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; public class LocalInnerClass { public static void main(String[] args) { Frame f = new Frame("登录"); //新建一个窗体 f.setVisible(true); //设置是否显示 f.setSize(300,200); //设置大小 class MyWindowListener implements WindowListener { //局部内部类 @Override public void windowOpened(WindowEvent e) { } @Override public void windowClosing(WindowEvent e) { System.out.println("关闭窗口"); System.exit(0); //关闭窗口操作 } @Override public void windowClosed(WindowEvent e) { } @Override public void windowIconified(WindowEvent e) { } @Override public void windowDeiconified(WindowEvent e) { } @Override public void windowActivated(WindowEvent e) { } @Override public void windowDeactivated(WindowEvent e) { } } MyWindowListener l = new MyWindowListener(); f.addWindowListener(l); //窗口监听器,需要传入一个实现WindowListener接口的类类型 } } ``` ### 匿名内部类 匿名内部类由于没有名字,只能使用一次。创建格式如下: ```java new 父类构造器(参数列表)|实现接口() { //匿名内部类的类体部分 } ``` 在这里我们看到使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。这个引用是隐式的。 > 在使用匿名内部类的过程中,我们需要注意如下几点: > > 1. 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能 > 继承一个类或者实现一个接口。 > 2. 匿名内部类中是不能定义构造函数的。 > 3. 匿名内部类中不能存在任何的静态成员变量和静态方法。 > 4. **匿名内部类为局部内部类**,所以局部内部类的所有限制同样对匿名内部类生效。 > 5. 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 > 6. 和局部内部类一样,只能访问final型的局部变量,因为内部类会被单独编译成一个字节码文件,为了保障这个单独的文件中用到的内部类外部的变量与内部类外部的变量的值绝对一致,系统从规则上限制这个值不可以被更改。 ```java //Person接口 public interface Person { void say(); } //匿名内部类演示 public class NoNameInnerClass { public static void main(String[] args) { final int a = 10; //jdk1.8以后可以省略final int b = 1; b = 2; //明显b不是final型的 Person p = new Person() { //匿名内部类 实现了Person接口 @Override public void say() { System.out.println("匿名内部类中的say方法" + a); //System.out.println(b); //报错,局部内部类和匿名内部类不能访问非final型的局部变量 } }; ha(p); } public static void ha(Person p){ } } ``` ### 静态内部类 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。 静态内部类是不需要依赖于外部类对象的,这点和类的静态成员属性有点类似,并且它**不能使用外部类的非** **static成员变量或者方法.** ```java public class Test { public static void main(String[] args) { StaticInnerClass.y = 100; Outter.Inner inner = new Outter.Inner(); inner.say(); //100.0 } } class Outter { private double x = 0; public static double y = 10; public Outer(double x,double y) { this.x = x; } class Inner { //成员内部类 public void say() { //System.out.println(x); //报错,静态内部类不能访问外部类的非静态成员和方法 System.out.println(y); } } } ``` ## 包装类 在Java中有一个设计的原则“一切皆对象”,那么这样一来Java中的一些基本的数据类型,就完全不符合于这种设计思想,因为Java中的八种基本数据类型并不是引用数据类型,所以Java中为了解决这样的问题,引入了八种基本数据类型的包装类。 | 基本数据类型 | 包装类 | | ------------ | --------- | | int | Integer | | char | Character | | float | Float | | double | Double | | boolean | Boolean | | byte | Byte | | short | Short | | long | Long | 以上的八种包装类,可以将基本数据类型按照类的形式进行操作。 以上的八种包装类也分为两种大的类型: * Number:Integer、Short、Long、Double、Float、Byte都是Number的子类表示是一个 数字。 * Object:Character、Boolean都是Object的直接子类。 ### 装箱和拆箱操作 以下以`Integer`和`Float`为例进行操作 将一个**基本数据类型变为包装类,那么这样的操作称为装箱操作**。 将一个**包装类变为一个基本数据类型,这样的操作称为拆箱操作**, 因为所有的数值型的包装类都是Number的子类,Number的类中定义了如下的操作方法,以下的全部方法都 是进行拆箱的操作。 | 方法 | 描述 | | ------------------------------------ | ------------------ | | public byte byteValue() | 用于Byte->byte | | public abstract double doubleValue() | 用于Double->double | | public abstract float floatValue() | 用于Float->float | | public abstract int intValue() | 用于Integer->int | | public abstract long longValue() | 用于Long->long | | public short shortValue() | 用于Short->short | **装箱操作:** 在JDK1.4之前 ,如果要想装箱,直接使用各个包装类的构造方法即可,例如: ```java int temp = 10 ; // 基本数据类型 Integer x = new Integer(temp) ; // 将基本数据类型变为包装类 ``` 在JDK1.5,Java新增了自动装箱和自动拆箱,而且可以直接通过包装类进行四则运算和自增自减操作。例 如: ```java Float f = 10.3f ; // 自动装箱 float x = f ; // 自动拆箱 System.out.println(f * f) ; // 直接利用包装类完成 System.out.println(x * x) ; // 直接利用包装类完成 ``` ### 字符串转换 使用包装类还有一个很优秀的地方在于:可以将一个字符串变为指定的基本数据类型,此点一般在接收输入 数据上使用较多。 在`Integer`类中提供了以下的操作方法: `public static int parseInt(String s)` :将String变为int型数据 在`Float`类中提供了以下的操作方法: `public static float parseFloat(String s) `:将String变为Float 在`Boolean` 类中提供了以下操作方法: `public static boolean parseBoolean(String s) `:将String变为boolean …… ```java public class demo11 { public static void main(String[] args) { Scanner input = new Scanner(System.in); String text = input.nextLine(); int x = Integer.parseInt(text); //转为int类型,便于运算 System.out.println(x+1); } } ```