JAVA 第三章 —— 继承

2019-03-22  本文已影响0人  定格r

1. 继承

1.1继承的概念

继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

1.2类的继承格式
class 父类 {
}
 
class 子类 extends 父类 {
}

公共父类:

public class Animal { 
    private String name;  
    private int id; 
    public Animal(String myName, int myid) { 
        name = myName; 
        id = myid;
    } 
    public void eat(){ 
        System.out.println(name+"正在吃"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡");
    }
    public void introduction() { 
        System.out.println("大家好!我是"         + id + "号" + name + "."); 
    } 
}

这个Animal类就可以作为一个父类,然后企鹅类和老鼠类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码) 继承之后的代码:

企鹅类:

public class Penguin extends Animal { 
    public Penguin(String myName, int myid) { 
        super(myName, myid);   //super 调用父类构造方法
    } 
}

老鼠类:

public class Mouse extends Animal { 
    public Mouse(String myName, int myid) { 
        super(myName, myid); //super 调用父类构造方法,并向其传参数
    } 
}
1.3继承类型

注意:需要注意的是 Java 不支持多继承,但支持多重继承。

image.png
1.4继承的特性
1.5 继承关键字

继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。

extends关键字

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。

public class Animal { 
    private String name;   
    private int id; 
    public Animal(String myName, String myid) { 
        //初始化属性值
    } 
    public void eat() {  //吃东西方法的具体实现  } 
    public void sleep() { //睡觉方法的具体实现  } 
} 
 
public class Penguin  extends  Animal{ 
}

implements关键字

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

public interface A {
    public void eat();
    public void sleep();
}
 
public interface B {
    public void show();
}
 
public class C implements A,B {
}

super 与 this 关键字

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

this关键字:指向自己的引用。

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 调用自己的方法
    super.eat();  // super 调用父类方法
  }
}

final关键字

final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:
声明类:

final class 类名 {//类体}

声明方法:

修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}

注:实例变量也可以被定义为 final,被定义为 final 的变量不能被修改。被声明为 final 类的方法自动地声明为 final,但是实例变量并不是 final

2. Object:所有类的超类

2.1 概念

Obje类是所有Java类的祖先。每个类都使用 Object 作为超类。所有对象(包括数组)都实现这个类的方法。
在不明确给出超类的情况下,Java会自动把Object作为要定义类的超类。
可以使用类型为Object的变量指向任意类型的对象。
Object类是Java中唯一没有父类的类
Object类有一个默认构造方法pubilc Object(),在构造子类实例时,都会先调用这个默认构造方法。

2.2 Object中几个重要的方法:

1)基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean 他们之间的比较,用双等号(==),比较的是他们的值。

2)复合数据类型(类) 当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。

hashCode 的常规协定是(重写equals()时,必须重写hashcode()并保证此协定):

    如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等
    如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同
    如果两个对象的hashcode值不等,则equals方法得到的结果必定为false
    如果两个对象的hashcode值相等,则equals方法得到的结果未知
public static void main(String[] args) {
        Main m = new Main();
        System.out.println(m.toString());  //complete.Main@7852e922
        System.out.println(m);   //complete.Main@7852e922
    }

注意: java 是开源,源代码公开,,,

查看源代码方式:

方式一:按住 Ctrl 键,单机你需要看的源代码。
方式二:把光标移动到你需要查看代码处,按下 F3

为什么要查看源代码?

1.查看源代码可以了解别人是如何写出这个技术的,让我们了解的更加的深入。
2.吸收大牛思想。

toString() 方法有何作用?
重写 toString() 之后,我们直接输出一个对象的时候,就会输出符合我们所需求的格式数据


class person {
    String name;
    int age;

    public person(String name, int age) {
        this.name = name;
        this.age = age;
    }
//重写 toString() 方法
    public String toString() {             
        return "姓名:" + this.name + " 年龄:" + this.age;
    }
}
public class Main {

    public static void main(String[] args) {
        person p = new person("haha", 18);
        System.out.println(p);
    }
}

结果:

image.png

3. 泛型

3.1 泛型定义:

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

例如:

需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案:是可以使用 Java 泛型。

3.2 泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

public class Main {
    // 泛型方法 printArray
    public static <E> void printArray(E[] arry) {
        // 输出数组元素
        for (int i = 0; i < arry.length; i++) {
            System.out.printf("%s ", arry[i]);
        }
        System.out.println();
    }
    public static void main(String args[]) {
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
        
        System.out.println("整型数组元素为:");
        printArray(intArray); // 传递一个整型数组

        System.out.println("\n双精度型数组元素为:");
        printArray(doubleArray); // 传递一个双精度型数组

        System.out.println("\n字符型数组元素为:");
        printArray(charArray); // 传递一个字符型数组
    }
}

结果:

image.png
3.3 泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

举例:

public class Box<T> {
   
  private T t;
 
  public void add(T t) {
    this.t = t;
  }
 
  public T get() {
    return t;
  }
 
  public static void main(String[] args) {
    Box<Integer> integerBox = new Box<Integer>();
    Box<String> stringBox = new Box<String>();
 
    integerBox.add(new Integer(10));
    stringBox.add(new String("菜鸟教程"));
 
    System.out.printf("整型值为 :%d\n\n", integerBox.get());
    System.out.printf("字符串为 :%s\n", stringBox.get());
  }
}

结果:


image.png

4. 对象包装器与自动打包

4.1 对象包装器

所有基本类型都有一个与之对应的类,例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapping)

这些对象包装器拥有很鲜明的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前面六个类派生于公共的超类Number)。对象包装器类是不可变的,一旦构造了包装器,就不允许更改包装器里面的值。同时,对象包装器还是final,因此不能定义他们的子类。

4.2 自动打包

定义一个整形数组列表,尖括号里面的参数类型不允许是基本类型,也就是说不允许写成ArrayList<int>。这里就用到了Integer对象包装器类。我们可以声明一个Integer对象的数组列表。

         ArrayList<Integer> list = new ArrayList<Integer>;

当调用:

         list.add(3);

将自动变换成:

         list.add(new Integer(3));

这种变换称之自动打包(autoboxing)

相反的如果将Integer对象赋值给一个int值时,将会 自动第拆包 ,也就是说编译器将下列语句:

int n = list.get(i);

翻译成:

int n = list.get(i).intValue();

甚至在算术表达式中也能够自动的打包和拆包。例如,可以将自增操作符应用于一个包装器引用:

Integer n = 3;
n++;

编译器将自动第插入一条拆开对象包的指令,然后进行自增运算,最后再将结果打入对象包内。
很多情况下,容易有一种假象,即基本类型和他们的对象包装类是一样的,只是他们的相等性不同,==运算符也可以用于包装器对象,只不过检测的是对象是否指向同一个存储区域。

先看下面一段代码:

public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

输出结果为:true,false。结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。下面这段代码是Integer的valueOf方法的具体实现:

public static Integer valueOf(int i) {
        if(i >= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[i + 128];
        else
            return new Integer(i);
    }

自动打包要求规范要求boolean、byte、char<=127,介于-128~127之间的short和int被包装到固定的对象中,否则创建一个新的对象。如上面的i1和i2初始化为100,对他们的比较的结果一定是成立的。

所以在比较两个包装器对象的值是否相等时调用equals方法就能避免出现false的情况。

最后说明一下,打包和拆包是编译器认可的,而不是虚拟机。编译器在生成类的字节码时插入必要的方法调用。虚拟机只是执行这些字节码。

5.参数数量可变的方法

可变的参数类型,也称为不定参数类型。英文缩写是varargus,还原一下就是variable argument type。通过它的名字可以很直接地看出来,这个方法在接收参数的时候,个数是不定的。
Java开始支持可变参数的方法,比如printf方法就是一个可变参数的方法。

我们也可以自定义可变参数的方法,具体语法为:

  返回类型 函数名(参数类型...参数名字)

原理:就是将接收的参数组装成一个临时数组,然后再处理临时数组中的数据。

示例代码:

public class Main {

    public static double max(double... values) { // //定义可变参数 values
        double largest = Double.MIN_VALUE;  ////次数为 Double 能表示的最小值
        
        for (double v : values) { // 相当于把values数组里的每一个值取出赋于v,直到结束
            
           largest = v > largest ? v : largest; // 取大值
        }
        
        return largest;
    }

    public static void main(String[] args) {
        double m = max(1.2, 3.4, 56.7, 345.45, 2, 5);
        System.out.println(m);
    }

}

结果:


image.png

这段代码接受任意个double类型的参数,从中寻找其中的最大值并在控制台输出。

要点:

  1. 在方法中定义可变参数后,我们可以像操作数组一样操作该参数;

  2. 如果该方法除了可变参数还有其它的参数,可变参数必须放到最后;

  3. 调用使用了可变参数的方法时:

    a. 可以不写参数,即传入空参;
    b. 可以直接在里边写入参数,参数间用逗号隔开;
    c. 可以传入一个数组;

6.枚举类

6.1 枚举的定义

这是在没有枚举类型时定义常量常见的方式

//使用普通方式定义日期常量
 
public class DayDemo {

    public static final int MONDAY =1;

    public static final int TUESDAY=2;

    public static final int WEDNESDAY=3;

    public static final int THURSDAY=4;

    public static final int FRIDAY=5;

    public static final int SATURDAY=6;

    public static final int SUNDAY=7;

}

上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量

//枚举类型,使用关键字enum
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。

枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。那么该如何使用呢?如下:

public class EnumDemo {

    public static void main(String[] args){
        //直接引用
        Day day =Day.MONDAY;
    }

}
//定义枚举类型
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

就像上述代码那样,直接引用枚举的值即可,这便是枚举类型的最简单模型。

6.2 枚举类的定义

枚举类和普通类有以下几个不同点:

1、枚举类不能指定继承的父类(因为继承了java.lang.Enum类),但是可以实现多个接口, 枚举类默认实现了Comparable接口和Serializable接口

2、枚举类的构造方法的访问权限只可为private

3、枚举类的实例必须显式列出。

6.3 枚举类的真正实现

举例:

public enum Season {
    SPRING("春天"), SUMMER("夏天"), AUTUMN("秋天"), WINTER("冬天");
    
    private final String chinese;
    
    private Season(String chinese) {
        this.chinese = chinese;
    }

    public String getChinese() {
        return chinese;
    }
}

其实上述代码相当于:

public final class Season extends Enum<Season> {
    public static final Season SPRING;
    public static final Season SUMMER;
    public static final Season AUTUMN;
    public static final Season WINTER;
    private static final Season[] ENUM$VALUES;
    static {
        SPRING = new Season("SPRING", 0, "春天");
        SUMMER = new Season("SUMMER", 1, "夏天");
        AUTUMN = new Season("AUTUMN", 2, "秋天");
        WINTER = new Season("WINTER", 3, "冬天");
        ENUM$VALUES = new Season[]{SPRING, SUMMER, AUTUMN, WINTER}
    }
    
    private final String chinese;
    
    private Season(String name, int ordinal, String chinese) {
        super(name, ordinal);
        this.chinese = chinese;
    }

    public String getChinese() {
        return chinese;
    }
    
    public static Season[] values() {
        Season[] arr = new Session[ENUM$VALUES.length];
        System.arraycopy(ENUM$VALUES, 0, arr, 0, arr.length);
        return arr;
    }
    
    public static Season valueOf(String name) {
        return Enum.valueOf(Season.class, name);
    }
}

在编译过程中,编译器会对枚举类进行以下处理:

1、增加一个静态初始化块和一个静态数组,静态初始化块负责构造枚举类的实例并将其保存到数组中。
2、修改当前枚举类的构造方法,向构造方法前面增加两个参数:name, ordinal。分别代表枚举类的名称和初始化顺序,并且使用super调用父类java.lang.Enum的构造方法。
3、添加两个静态方法:
public static Season[] values()
该方法返回一个数组,包含了该枚举类所有的实例(并不是返回枚举类自己保存实例的数组,而是通过对其复制,以确保实例不被篡改)。
public static Season valueOf(String name)
该方法通过当前枚举类的名称返回对应的实例。比如valueOf(“SPRING”)返回Season.SPRING)。

7.反射

7.1 反射的概念

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

反射就是把java类中的各种成分映射成一个个的Java对象

例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
如图是类的正常加载过程:反射的原理在与class对象。
熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。

image.png
上一篇 下一篇

猜你喜欢

热点阅读