java(二) 面向对象


一. 面向对象

1.1 面向过程和面向对象

面向过程(Procedure Oriented)和面向对象(Object Oriented,OO)都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。

早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显的显示出来,出现了面向对象思想并成为目前主流的方式。两者都贯穿于软件分析、设计和开发各个阶段,对应面向对象就分别称为面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。

C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。

面向对象具有三大特征:封装性、继承性和多态性

1.2 对象和类

**类:**我们叫做class。 **对象:**我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

总结

  1.对象是具体的事物;类是对对象的抽象;

  2.类可以看成一类对象的模板,对象可以看成该类的一个具体实例。

  3.类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。

类的定义:

// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Car { 
}
class Tyre { // 一个Java文件可以同时定义多个class
}
class Engine {
}
class Seat {
}

上面的类定义好后,没有任何的其他信息,就跟我们拿到一张张图纸,但是纸上没有任何信息,这是一个空类,没有任何实际意义。所以,我们需要定义类的具体信息。

对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个。

简单的学生类编写:

public class student {
    //属性(成员变量)
    int id;
    String sname;
    int age;  
    //方法
    void study(){
        System.out.println("我正在学习!");
    }  
    //构造方法,与类名名字相同的方法
    student(){
    }
}
  • 属性:用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。

在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化。

成员变量的默认值:

数据类型默认值
整型0
浮点型0.0
字符型'\u0000'
布尔型false
所有引用类型null

1.3 面向对象的内存分析

Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。

栈的特点如下:

  1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)

  2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)

  3. 栈属于线程私有,不能实现线程间的共享!

  4. 栈的存储特性是“先进后出,后进先出”

  5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!

堆的特点如下:

  1. 堆用于存储创建好的对象和数组(数组也是对象)

  2. JVM只有一个堆,被所有线程共享

  3. 堆是一个不连续的内存空间,分配灵活,速度慢!

方法区(又叫静态区)特点如下:

  1. JVM只有一个方法区,被所有线程共享!

  2. 方法区实际也是堆,只是用于存储类、常量相关的信息!

  3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、静态方法、字符串常量等)

1.4 构造方法

构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。

[修饰符] 类名(形参列表){
    //n条语句
}

要点:

  1. 通过new关键字调用!!

  2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。

  3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!

  4. 构造器的方法名必须和类名一致!

1.5 构造方法的重载

构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。

满足任一条件构成重载:1.形参类型,2.形参个数,3.形参顺序不同

构造方法重载(创建不同用户对象):

public class User {
    int id; // id
    String name; // 账户名
    String pwd; // 密码
    public User() {
 
    }
    public User(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }
    public static void main(String[] args) {
        User u1 = new User();
        User u2 = new User(101, "test1");
        User u3 = new User(100, "test2", "123456");     
    }
}
  • 如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。

this表示当前对象,this.id 表示属性id,id表示形参id

1.6 垃圾回收机制

Garbage Collection,简称GC

内存管理:

Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。

  • 对象空间的分配:使用new关键字创建对象即可

  • 对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。

垃圾回收过程:

  任何一种垃圾回收算法一般要做两件基本事情:

  1. 发现无用的对象

  2. 回收无用对象占用的内存空间。

垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理

1.7 通用的分代垃圾回收机制

分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。

JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。

  1. 年轻代

  所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。

  1. 年老代

  在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

  1. 持久代

  用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。

  • Minor GC:

  用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)

  • Major GC:

  用于清理老年代区域。

  • Full GC:

  用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。

垃圾回收过程:

​ 1、新创建的对象,绝大多数都会存储在Eden中,

​ 2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,

​ 然后剩余对象复制到某个Survivor中(S1),同时清空Eden区

​ 3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中(S2),同时将Eden区中的不能清空 的对象,复制到S1中,保证Eden和S1,均被清空一次。

​ 4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,

​ 5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)

1.8 JVM调优和Full GC

在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

  1.年老代(Tenured)被写满

  2.持久代(Perm)被写满

  3.System.gc()被显式调用(程序建议GC启动,不是调用GC)

  4.上一次GC之后Heap的各域分配策略动态变化

1.9 开发中容易造成内存泄露的操作

以下四种情况时最容易造成内存泄露,造成系统的崩溃:

(1) 创建大量无用对象

比如,在需要大量拼接字符串时,使用了String而不是StringBuilder。

String str = "";
for (int i = 0; i < 10000; i++) {   
    str += i;     //相当于产生了10000个String对象
}

(2) 静态集合类的使用

像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。

(3)各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭

IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。

(4) 监听器的使用

释放对象时,没有删除相应的监听器。

要点:

  1. 程序员无权调用垃圾回收器。

  2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。

  3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。

1.10 this关键字

对象创建的过程和this的本质:

构造方法是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量初始化为0或空

  2. 执行属性值的显示初始化

  3. 执行构造方法

  4. 返回对象的地址给相关的变量

this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。

this最常的用法:

  1. 在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。

  2. 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。

  3. this不能用于static方法中。   4. this调用本类中的其他方法;   5. 在方法中使用return this;返回类的当前对象的值。

this代表“当前对象”示例:

public class User {
    //成员变量
    int id;        //id
    String name;   //账户名
    String pwd;   //密码
 
    public User() {
    }
    public User(int id, String name) {
        System.out.println("正在初始化已经创建好的对象:"+this);
        this.id = id;   //不写this,无法区分局部变量id和成员变量id
        this.name = name;
    }
    public void login(){
        //不写this效果一样,因为此方法中没有局部变量
        System.out.println(this.name+",登录!");
    }  
     
    public static void main(String[] args) {
        User  u3 = new User(101,"test");
        System.out.println("打印test对象:"+u3);
        u3.login();
    }
}

this()调用重载构造方法:

public class TestThis {
    int a, b, c;
 
    TestThis() {
        System.out.println("正要初始化一个Hello对象");
    }
    TestThis(int a, int b) {
        // TestThis(); //这样是无法调用构造方法的!
        this(); // 调用无参的构造方法,并且必须位于第一行!
        a = a;// 这里都是指的局部变量而不是成员变量
        this.a = a;// 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!
        this.b = b;
    }
    TestThis(int a, int b, int c) {
        this(a, b); // 调用带参的构造方法,并且必须位于第一行!
        this.c = c;
    }
 
    void sing() {
    }
    void eat() {
        this.sing(); // 调用本类中的sing();
        System.out.println("hello,eat()+sing()");
    }
 
    public static void main(String[] args) {
        TestThis hi = new TestThis(2, 3);
        hi.eat();
    }
}

1.11 static 关键字

在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:

  1. 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。

  2. 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!

  3. 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)

  4. 在static方法中不可直接访问非static的成员,要先实例化,再使用非static成员.。

5. static修饰的成员变量和方法,从属于类;普通变量和方法从属于对象的

static关键字的使用:

6. 非static可直接互相调用,普通方法内可直接调用static方法和static属性

public class User {
    int id; // id
    String name; // 账户名
    String pwd; // 密码
     
    static String company = "testCompany"; // 公司名称
     
     
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
     
    public void login() {
        printCompany();
        System.out.println(company); 
        System.out.println("登录:" + name);
    }
     
    public static void printCompany() {
        //login();//在静态方法内直接调用非静态方法,编译就会报错
        User uu=new User();
        uu.login();//先实例化,再调用
        System.out.println(company);
    }
     
    public static void main(String[] args) {
        User u = new User(101, "test");
        User.printCompany();
        User.company = "testCompany2";
        User.printCompany();
    }
}

1.12 静态代码块

构造方法用于对象的初始化! 静态代码块,用于类的初始化操作!在静态代码块中不能直接访问非static成员。

静态代码块执行顺序(学完继承再看这里):

  1. 上溯到Object类,先执行Object的静态代码块,再向下执行子类的静态代码块,直到我们的类的静态代码块为止。

  2. 构造方法执行顺序和上面顺序一样!!

public class User3 {
    int id;        //id
    String name;   //账户名
    static {
        System.out.println("静态代码块,执行类的初始化工作");
        print();
    }  
    public static void print(){
        System.out.println("打印....");
    }  
    public static void main(String[] args) {
        User3  u3 = new User3();
    }
}

当类被调用,自动运行代码块和静态代码块。

public class test2 {
    public static void main(String[] args) {
        new a();//调用a类
    }
}

class a{
    a(){
        System.out.println("构造方法,名字和类名一致");
    }

    static{
        System.out.println("静态代码块,被static修饰");
    }

    {
        System.out.println("普通代码块,无修饰词");
    }
}

运行结果:

​ 静态代码块,被static修饰 ​ 普通代码块,无修饰词 ​ 构造方法,名字和类名一致

运行顺序:

静态代码块>普通代码块>构造方法(构造代码块)

静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次。

构造代码块在每次创建对象时都会执行,不用static修饰,与方法名相同。

public class test3 {
    public static void main(String[] args) {
        new test();//调用两次test类
        new test();
    }
}

class test{

    test(){
        System.out.println("我是构造方法");
    }
    static{
        System.out.println("我是静态代码块,只执行一次");
    }
}

运行结果(静态代码块只执行了一次):

我是静态代码块,只执行一次 我是构造方法 我是构造方法

1.13 静态导入

静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

 //以下两种静态导入的方式二选一即可
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性
 
public class Test2{
    public static void main(String [] args){
        //直接使用静态属性
        System.out.println(PI);
        System.out.println(random());
    }
}

二. 面向对象深入

对面向对象的三大特征:继承、封装、多态进行详细的讲解。另外还包括抽象类、接口、内部类等概念。

2.1 继承的实现

继承让我们更加容易实现类的扩展,实现了代码的重用;关键字extends

子类继承父类后,子类拥有父类的属性和方法。

使用extends实现继承:

public class Test{
    public static void main(String[] args) {
        Student s = new Student("test",172,"Java");
        s.rest();
        s.study();
    }
}
class Person {
    String name;
    int height;
    public void rest(){
        System.out.println("Person,休息一会!");
    }
}
class Student extends Person {
    String major; //专业
    public void study(){
        System.out.println("Student,学习Java");
    }
    public Student(String name,int height,String major) {
        //天然拥有父类的属性
        this.name = name;
        this.height = height;
        this.major = major;
    }
}

2.2 继承使用要点

1.父类也称作超类、基类、派生类等。

2.Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。

3.Java中类没有多继承,接口有多继承。

4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法不能直接访问)。

5.如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。

2.3 instanceof 运算符

instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。

public class Test{
    public static void main(String[] args) {
        Student s = new Student("test",172,"Java");
        System.out.println(s instanceof Person);
        System.out.println(s instanceof Student);
    }
}

两条语句的输出结果都是true。

2.4 方法的重写override

子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。

方法的重写需要符合下面的三个要点:

​ 1.“==”: 方法名、形参列表相同。

​ 2.“≤”:返回值类型和声明异常类型,子类小于等于父类。

​ 3.“≥”: 访问权限,子类大于等于父类。

方法重写:

public class TestOverride {
    public static void main(String[] args) {
        Vehicle v1 = new Vehicle();
        Vehicle v2 = new Plane();
        v1.run();
        v2.run();
        v2.stop();
    }
}
 
class Vehicle { // 交通工具类
    public void run() {
        System.out.println("跑....");
    }
    public void stop() {
        System.out.println("停止不动");
    }
}
 
class Plane extends Vehicle {
    public void run() { // 重写父类方法
        System.out.println("天上飞!");
    }
    public void stop() {// 重写父类方法
        System.out.println("空中不能停,坠毁了!");
    }
}  

2.5 Object类基本特性

Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。

public class Person {
    ...
}
//等价于:
public class Person extends Object {
    ...
}

2.6 toString方法

Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

toString()方法测试和重写toString()方法:

class Person {
    String name;
    int age;
    @Override
    public String toString() {
        return name+",年龄:"+age;
    }
}
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        p.age=20;
        p.name="test";
        System.out.println("info:"+p);
         
        Test t = new Test();
        System.out.println(t);
    }
}

运行结果:

info:test,年龄:20 Test@4554617c

2.7 ==和equals方法

“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。

Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。

ublic class TestEquals { 
    public static void main(String[] args) {
        Person p1 = new Person(123,"test1");
        Person p2 = new Person(123,"test2");     
        System.out.println(p1==p2);     //false,不是同一个对象
        System.out.println(p1.equals(p2));  //true,id相同则认为两个对象内容相同
        String s1 = new String("测试");
        String s2 = new String("测试");
        System.out.println(s1==s2);         //false, 两个字符串不是同一个对象
        System.out.println(s1.equals(s2));  //true,  两个字符串内容相同
    }
}
class Person {
    int id;
    String name;
    public Person(int id,String name) {
        this.id=id;
        this.name=name;
    }
    public boolean equals(Object obj) {//重写equals方法
        if(obj == null){
            return false;
        }else {
            if(obj instanceof Person) {
                Person c = (Person)obj;
                if(c.id==this.id) {
                    return true;
                }
            }
        }
        return false;
    }
}

2.8 super关键字

  • super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。

  • 使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

  • 若是构造方法的第一行代码没有显式的调用super(...)或者this(...);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。

super关键字的使用:

public class TestSuper01 { 
    public static void main(String[] args) {
        new ChildClass().f();
    }
}
class FatherClass {
    public int value;
    public void f(){
        value = 100;
        System.out.println ("FatherClass.value="+value);
    }
}
class ChildClass extends FatherClass {
    public int value;
    public void f() {
        super.f();  //调用父类对象的普通方法
        value = 200;
        System.out.println("ChildClass.value="+value);
        System.out.println(value);
        System.out.println(super.value); //调用父类对象的成员变量
    }
}

运行结果:

FatherClass.value=100 ChildClass.value=200 200 100

2.9 封装

程序设计要追求“高内聚,低耦合”。 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

编程中封装的具体优点:

​ 1. 提高代码的安全性。

​ 2. 提高代码的复用性。

​ 3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。

​ 4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

封装的实现—使用访问控制符

Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java中4种“访问控制符”分别为private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

访问权限范围:

| 修饰符 | 同一个类 | 同一个包 | 子类 | 所有类 | | :-------: | :------: | :------: | :--: | :----: | | private | * | | | | | default | * | * | | | | protected | * | * | * | | | public | * | * | * | * |

​ 1. private 表示私有,只有自己类能访问

​ 2. default表示没有修饰符修饰,只有同一个包的类能访问

​ 3. protected表示可以被同一个包的类以及其他包中的子类访问

​ 4. public表示可以被该项目的所有包中的所有类访问

封装的使用细节:

类的属性的处理:

​ 1. 一般使用private访问权限。

​ 2. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。

​ 3. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。

public class Person {
    // 属性一般使用private修饰
    private String name;
    private int age;
    private boolean flag;
//为属性提供public修饰的set/get方法,供外界其它方法调用
//外界无法通过person.age操作属性,因为属性被private修饰但可以通过person.setAge()方法操作
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

2.10 多态

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

多态的要点:

​ 1. 多态是方法的多态,不是属性的多态(多态与属性无关)。

2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。

​ 3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

多态和类型转换测试:

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
    public void seeDoor() {
        System.out.println("看门中....");
    }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
        animalCry(a1);//输出喵喵喵喵!
        Animal a2 = new Dog();
        animalCry(a2);//Animal a2为编译类型,Dog对象才是运行时类型。
         
        //编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
        // 否则通不过编译器的检查。
        Dog dog = (Dog)a2;//向下需要强制类型转换
        dog.seeDoor();
    }
 
    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    static void animalCry(Animal a) {
        a.shout();
    }
 
    /* 如果没有多态,我们这里需要写很多重载的方法。
     * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
    static void animalCry(Dog d) {
        d.shout();
    }
    static void animalCry(Cat c) {
        c.shout();
    }*/
}

2.11 对象的转型(casting)

父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。

向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!

public class TestCasting {
    public static void main(String[] args) {
        //obj编译类型为Object,运行类型为String
        Object obj = new String("测试"); // 向上可以自动转型
        // obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
        /* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
         * 不然通不过编译器的检查。 */
        String str = (String) obj; // 向下转型
        System.out.println(str.charAt(0)); // 位于0索引位置的字符
        System.out.println(obj == str); // true.他们俩运行时是同一个对象
    }
}

2.12 final关键字

  • 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final int MAX_SPEED = 120;
  • 修饰方法:该方法不可被子类重写。但是可以被重载!
final void  study(){}
  • 修饰类: 修饰的类不能被继承。比如:Math、String等。
final  class  A {}

2.13 抽象方法和抽象类

抽象方法:

​ 使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

抽象类:

​ 包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

public abstract class Animal {

    //被abstract修饰的方法,当前类不实现,子类必须实现
    abstract void shout();
}

class Dog extends Animal{

    @Override
    void shout() {
        System.out.println("汪汪汪");
    }
}

抽象类的使用要点:

​ 1. 有抽象方法的类只能定义成抽象类

​ 2. 抽象类不能实例化,即不能用new来实例化抽象类。

​ 3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。

​ 4. 抽象类只能用来被继承。

​ 5. 抽象方法必须被子类实现。

2.14 接口

  • 接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

  • 抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。

  • 从接口的实现者角度看,接口定义了可以向外部提供的服务。

  • 从接口的调用者角度看,接口定义了实现者能提供那些服务。

接口的本质:

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。

区别

​ 1. 普通类:具体实现

​ 2. 抽象类:具体实现,规范(抽象方法)

3. 接口:规范!

2.15 定义和使用接口

定义接口的详细说明:

​ 1. 访问修饰符:只能是public或默认。

​ 2. 接口名:和类名采用相同命名机制。

​ 3. extends:接口可以多继承。

​ 4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。

​ 5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

要点:

​ 1. 子类通过implements来实现接口中的规范。

​ 2. 接口不能创建实例,但是可用于声明引用变量类型。

​ 3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。

​ 4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。

  5. JDK1.8后,接口中包含普通的静态方法。

接口的多继承:

接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。

interface A {
    void testa();
}
interface B {
    void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
    void testc();
}
/*Test方法实现C接口,同时可以实现A和B接口的方法*/
public class Test implements C {
    public void testc() {
    }
    public void testa() {
    }
    public void testb() {
    }
}

案例:

public class TestInterface {
    public static void main(String[] args) {
        Volant volant = new Angel();
        volant.fly();
        System.out.println(Volant.FLY_HIGHT);
         
        Honest honest = new GoodMan();
        honest.helpOther();
    }
}
/**飞行接口*/
interface Volant { 
    int FLY_HIGHT = 100;  // 总是:public static final类型的;
    void fly();   //总是:public abstract void fly();
}
/**善良接口*/
interface Honest { 
    void helpOther();
}
/**Angle类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{
    public void fly() {
        System.out.println("我是天使,可以飞!");
    }
    public void helpOther() {
        System.out.println("帮助别人,很善良!");
    }
}
class GoodMan implements Honest {
   public void helpOther() {
        System.out.println("好人会帮助别人!");
    }  
}

2.16 内部类

把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

注意:

内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

/**外部类Outer*/
class Outer {
    private int age = 10;
    public void show(){
        System.out.println(age);//10
    }
    /**内部类Inner*/
    public class Inner {
        //内部类中可以声明与外部类同名的属性与方法
        private int age = 20;
        public void show(){
            System.out.println(age);//20
        }
    }
}

内部类的作用:

​ 1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。

​ 2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。

​ 3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。

成员变量访问要点:

​ 1. 内部类里方法的局部变量:变量名。

​ 2. 内部类属性:this.变量名。

​ 3. 外部类属性:外部类名.this.变量名。

class Outer {
    private int age = 10;
    class Inner {
        int age = 20;
        public void show() {
            int age = 30;
            System.out.println("内部类方法里的局部变量age:" + age);// 30
            System.out.println("内部类的成员变量age:" + this.age);// 20
            System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
        }
    }
}

静态内部类:

class Outer{
    //相当于外部类的一个静态成员
    static class Inner{
    }
}
public class TestStaticInnerClass {
    public static void main(String[] args) {
        //通过 new 外部类名.内部类名() 来创建内部类对象
        Outer.Inner inner =new Outer.Inner();
    }
}

局部内部类:

​ 定义在方法内部的,作用域只限于本方法,称为局部内部类。

​ 局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

​ 局部内部类在实际开发中应用很少。

public class Test2 {
    public void show() {
        //作用域仅限于该方法
        class Inner {
            public void fun() {
                System.out.println("helloworld");
            }
        }
        new Inner().fun();
    }
    public static void main(String[] args) {
        new Test2().show();//输出helloworld
    }
}

2.17 String类

String类又称作不可变字符序列,位于java.lang包中,Java程序默认导入java.lang包下的所有类。

基本使用:

public class test5 {
    public static void main(String[] args) {
        String s1 = ""; //定义空字符串
        String s2 = "Hello";
        String s3 = "World! ";
        System.out.println(s2+s3);//使用加号拼接字符串,输出HelloWorld!
        String s4="abc";
        String s5= s4+1;
        //+运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。
        System.out.println(s5);//输出abc1
    }
}

常量池:

public class test5 {
    public static void main(String[] args) {
        String s1 = "abc";//将s1放入全局字符串常量池(String Pool)
        String s2 = "abc";//将s2放入全局字符串常量池(String Pool)
        String s3=new String("abc");//新建了一个String对象
        System.out.println(s1==s2);//输出true
        System.out.println(s1==s3);//输出false,双等号比较是否是同一个对象
        System.out.println(s1.equals(s3));//输出true,equals比较值是否相等
    }
}

通常比较字符串时,使用equals

2.18 String类常用的方法

public class StringTest1 {
    public static void main(String[] args) {
        String s1 = "core Java";
        String s2 = "Core Java";
        System.out.println(s1.charAt(3));//e,提取下标为3的字符
        System.out.println(s2.length());//9,字符串的长度
        System.out.println(s1.equals(s2));//false,比较两个字符串是否相等
        System.out.println(s1.equalsIgnoreCase(s2));//true,比较两个字符串(忽略大小写)
        System.out.println(s1.indexOf("Java"));//5,字符串s1中是否包含Java,5为J的索引
        System.out.println(s1.indexOf("apple"));//-1,字符串s1中是否包含apple,不包含返回-1
        String s = s1.replace(' ', '&');//将s1中的空格替换成&
        System.out.println("result is :" + s);//result is :core&Java
    }
}
public class StringTest2 {
    public static void main(String[] args) {
        String s = "";
        String s1 = "How are you?";
        System.out.println(s1.startsWith("How"));//是否以How开头
        System.out.println(s1.endsWith("you"));//是否以you结尾
        s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
        System.out.println(s);
        s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7
        System.out.println(s);
        s = s1.toLowerCase();//转小写
        System.out.println(s);
        s = s1.toUpperCase();//转大写
        System.out.println(s);
        String s2 = "  How old are you!! ";
        s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
        System.out.println(s);
        System.out.println(s2);//因为String是不可变字符串,所以s2不变
    }
}

字符串相等的判断:

  1. equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
  2. 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
  3. 判断字符串是否相等不要使用"=="。
"Hello".equalsIgnoreCase("hellO");//返回true
基础知识
  • 作者:管理员(联系作者)
  • 发表时间:2019-12-21 03:42
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论