Java的面向对象

什么是面向对象

Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP。是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。

核心思想

以类的方式组织代码,以对象的方式组装数据。

定义class

一个类可以理解为自己定义了一个数据类型。
创建一个类,给这个类命名为Person,就是定义一个class。

    class Person {
        public String name;
        public int age;
    }

public是用来修饰字段的,它表示这个字段可以被外部访问。

静态属性和静态方法

有static标识符的,为静态属性或者方法,和类是一起加载的,

创建实例

new操作符可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:

Person ming = new Person(); //创建了一个Person类型的实例,并通过变量ming指向它。
//注意区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。
//有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问和赋值实例变量可以用变量.字段
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
方法

有时候public的属性在外面可以随意访问和修改,风险很大,所以可以定义成为私有属性,即:

class Person {
    private String name;
    private int age;
}
这样属性在外部就不能直接访问和修改,就需要调用方法才能操作。
public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setName("Xiao Ming"); // 设置name
        ming.setAge(12); // 设置age
        System.out.println(ming.getName() + ", " + ming.getAge());
    }
}
    
class Person {
    private String name;
    private int age;
    
    public String getName() {
        return this.name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return this.age;
    }
    
    public void setAge(int age) {
        if (age < 0 || age > 100) {
            throw new IllegalArgumentException("invalid age value");
        }
        this.age = age;
    }
}
私有方法
private方法不允许外部调用,是由内部方法是调用private方法。
this变量

this始终指向当前实例。如果没有命名冲突,可以省略this

    class Person {
        private String name;
        public String getName() {
            return name; // 相当于this.name
        }
    }
方法参数

方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。

可变参数
class Group {
    private String[] names;
    public void setNames(String... names) {
        this.names = names;
    }
}
//调用Group的实例的方法的时候可以传入0-多个参数
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames(); // 传入0个String
方法重点

方法可以让外部代码安全地访问实例字段;
方法是一组执行语句,并且可以执行任意逻辑;
方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同)
外部代码通过public方法操作实例,内部代码可以调用private方法;
理解方法的参数绑定。

构造方法

创建对象实例时就把内部字段全部初始化为合适的值,就需要构造方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Xiao Ming", 15); //在这里调用构造函数
            System.out.println(p.getName());
            System.out.println(p.getAge());
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {//这货就是构造方法
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
}

构造方法的名称就是类名。构造方法没有返回值(也没有void)
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句。所以之前new的时候后面带一个空的括号。如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法。
一个类里面可以有多个构造函数,便于代码的复用。

方法重载

如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。

class Hello {
    public void hello() {
        System.out.println("Hello, world!");
    }
    public void hello(String name) {
        System.out.println("Hello, " + name + "!");
    }
    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("Hi, " + name + "!");
        } else {
            System.out.println("Hello, " + name + "!");
        }
    }
}

另外,比如String类提供了多个重载方法indexOf()

    int indexOf(int ch):根据字符的Unicode码查找;
    int indexOf(String str):根据字符串查找;
    int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
    int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。
继承

Java使用extends关键字来实现类的继承,extends单次的意思就是扩展,可以简单的理解子类是父类的扩展。

    class Person {
        private String name;
        private int age;
        public String getName() {...}
        public void setName(String name) {...}
        public int getAge() {...}
        public void setAge(int age) {...}
    }
    class Student extends Person {
        // 不要重复name和age字段/方法,
        // 只需要定义新增score字段/方法:
        private int score;
        public int getScore() { … }
        public void setScore(int score) { … }
    }

因此通过继承,Student只需要编写额外的功能,不再需要重复代码。

继承树

上文中,Student继承自Person,而Person隐式的继承自Object。Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被(Override)

    class Person {
        protected String name;
        public final String hello() {
            return "Hello, " + name;
        }
    }
    Student extends Person {
        // compile error: 不允许覆写
        @Override
        public String hello() {
        }
    }
    //如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:
    final class Person {
        protected String name;
    }
    
    // compile error: 不允许继承自Person
    Student extends Person {
    }
多态

以下情况才能出现多态:

  1. 有继承关系
  2. 父类的引用指向子类的对象。
    所以出现同一方法根据对象的不同,产生不同的行为的情况。使程序更灵活。针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
    在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)
    class Person {
        public void run() {
            System.out.println("Person.run");
        }
    }
    class Student extends Person {
        @Override //可以让编译器帮助检查是否进行了正确的覆写。这是希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override不是必需的。
        public void run() {
            System.out.println("Student.run");
        }
    }

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。

覆写(Override)

在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
不能重写的情况:
1.static 方法,它属于类,不能在对象中被修改
2.final 在常量池中,不能被类修改
3.private 私有类,不能被覆写

protected(直译:受保护的)

继承有个特点,就是子类无法访问父类的private字段或者private方法。
这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问。

super超类

super关键字表示父类。使用super.name,或者this.name,或者name,效果都是一样的。编译器会自动定位到父类的name字段。
但是这货设计出来并非无用,子类的构造方法是编译器自动生成的,并不继承父类的构造方法,需要显示的调用。

    class Student extends Person {
        protected int score;
        public Student(String name, int age, int score) {
            super(name, age); // 调用父类的构造方法Person(String, int)
            this.score = score;
        }
    }
final

对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。
在构造方法中初始化final字段:

        class Person {
            public final String name;
            public Person(String name) {
                this.name = name;
            }
        }

可以保证实例一旦创建,其final字段就不可修改.

抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

        abstract class Person {
            public abstract void run();
        }

把一个方法声明为abstract,表示它是一个抽象方法,本身没有包含任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。必须把Person类本身也声明为abstract,才能正确编译它。
抽象类用的比较少,因为有更好用的interface。

面向抽象编程

尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
上层代码只定义规范(例如:abstract class Person);
不需要子类就可以实现业务逻辑(正常编译);
具体的业务逻辑由不同的子类实现,调用者并不关心。

接口

Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口interface。

    interface Person {
        void run();
        String getName();
    }

因为接口定义的所有方法默认都是public和abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class去实现一个interface时,需要使用implements关键字。

    class Student implements Person {
        private String name;
        public Student(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(this.name + " run");
        }
        @Override
        public String getName() {
            return this.name;
        }
    }
    //在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
    class Student implements Person, Hello { // 实现了两个interface
        ...
    }
接口继承

一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:

        interface Hello {
            void hello();
        }
        interface Person extends Hello {
            void run();
            String getName();
        }
此时,Person接口继承自Hello接口,因此,Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口。
继承关系

合理设计interface和abstract-class的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract-class中,具体逻辑放到各个子类,而接口层次代表抽象程度。
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

        List list = new ArrayList(); // 用List接口引用具体子类的实例
        Collection coll = list; // 向上转型为Collection接口
        Iterable it = coll; // 向上转型为Iterable接口
default方法

在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法。

    public class Main {
        public static void main(String[] args) {
                Person p = new Student("Xiao Ming");
                p.run();
        }
    }
    interface Person {
        String getName();
        default void run() {
            System.out.println(getName() + " run");
        }
    }
    class Student implements Person {
        private String name;
        public Student(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
    }

实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

接口的重点

Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
接口也是数据类型,适用于向上转型和向下转型;
接口的所有方法都是抽象方法,接口不能定义实例字段;
接口可以定义default方法(JDK>=1.8)。

实例字段

在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。

接口与抽象类对比
abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法
静态字段

用static修饰的字段,称为静态字段:static field。 实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。

class Person {
    public String name;
    public int age;
    // 定义静态字段number:
    public static int number;
}

对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例。虽然实例可以访问静态字段,但是它们指向的其实都是Person class的静态字段。所以,所有实例共享一个静态字段。因此,不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段(非实例字段)

静态方法

有静态字段,就有静态方法。用static修饰的方法称为静态方法。调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:

    public class Main {
        public static void main(String[] args) {
            Person.setNumber(99);
            System.out.println(Person.number);
        }
    }
    
    class Person {
        public static int number;
        public static void setNumber(int value) {
            number = value;
        }
    }

因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。

静态方法经常用于工具类。例如:

  • Arrays.sort()
  • Math.random()
接口的静态字段

因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:

    public interface Person {
        public static final int MALE = 1;
        public static final int FEMALE = 2;
    }

实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:

    public interface Person {
        // 编译器会自动加上public statc final:
        int MALE = 1;
        int FEMALE = 2;
    }

在Java中,我们使用package来解决名字冲突。Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名。
例如:
小明的Person类存放在包ming下面,因此,完整类名是ming.Person;
JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays。
所以在定义class的时候,我们需要在第一行声明这个class属于哪个包。
小明的Person.java文件:

        package ming; // 申明包名ming
        public class Person {
        }

在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用.隔开。例如:java.util。 要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
我们还需要按照包结构把上面的Java文件组织起来。所有Java文件对应的目录层次要和包的层次一致。编译后的.class文件也需要按照包结构存放。
在IDE中,会自动根据包结构编译所有Java源码,所以不必担心使用命令行编译的复杂命令。


包作用域

位于同一个包的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。例如,Person类定义在hello包下面:

    package hello;
    public class Person {
        // 包作用域:
        void hello() {
            System.out.println("Hello!");
        }
    }
    Main类也定义在hello包下面:
    package hello;
    public class Main {
        public static void main(String[] args) {
            Person p = new Person();
            p.hello(); // 可以调用,因为Main和Person在同一个包
        }
    }

import

在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类,他有三种写法:
第一种,直接写出完整类名但是每次写完整类名比较痛苦。
第二种写法是用import语句,导入小军的Arrays,然后写简单类名:
// Person.java
package ming;

    // 导入完整类名:
    import mr.jun.Arrays;
    public class Person {
        public void run() {
            Arrays arrays = new Arrays();
        }
    }

在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class),一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。
还有一种import static的语法,它可以导入可以导入一个类的静态字段和静态方法:

    package main;
    // 导入System类的所有静态字段和静态方法:
    import static java.lang.System.*;
    public class Main {
        public static void main(String[] args) {
            // 相当于调用System.out.println(…)
            out.println("Hello, world!");
        }
    }
    import static很少使用。

Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:
如果是完整类名,就直接根据完整类名查找这个class;
如果是简单类名,按下面的顺序依次查找:
查找当前package是否存在这个class;
查找import的包是否包含这个class;
查找java.lang包是否包含这个class。
如果按照上面的规则还无法确定类名,则编译报错。

我们来看一个例子:

    // Main.java
    package test;
    import java.text.Format;
    public class Main {
        public static void main(String[] args) {
            java.util.List list; // ok,使用完整类名 -> java.util.List
            Format format = null; // ok,使用import的类 -> java.text.Format
            String s = "hi"; // ok,使用java.lang包的String -> java.lang.String
            System.out.println(s); // ok,使用java.lang包的System -> java.lang.System
            MessageFormat mf = null; // 编译错误:无法找到MessageFormat: MessageFormat cannot be resolved to a type
        }
    }

因此,编写class的时候,编译器会自动帮我们做两个import动作:
默认自动import当前package的其他class;
默认自动import java.lang.*。
注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。
如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

实践中
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:
org.apache
org.apache.commons.log
子包就可以根据功能自行命名。
要注意不要和java.lang包的类重名,即自己的类不要使用这些关键字,也不要和JDK常用类重名:
String
System
Runtime
java.util.List
java.text.Format
java.math.BigInteger
...

作用域

在Java中,我们经常看到public、protected、private这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。
public:
定义为public的class、interface可以被其他任何类访问。
private:
定义为private的field(成员变量)、method无法被其他类访问。
确切地说,private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法。
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:
protected:
protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类。
package:
包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。只要在同一个包,就可以访问package权限的class、field和method。

作用域 当前类 同package 子孙类 其他package
public
protected X
friendly(default) X X
private X X X
局部变量

在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。也就是块级作用域。

final

final修饰符不是访问权限,它可以修饰class、field和method;
final与访问权限不冲突,它有很多作用。
用final修饰class可以阻止被继承.
用final修饰method可以阻止被子类覆写.
用final修饰field可以阻止被重新赋值.
用final修饰局部变量可以阻止被重新赋值.

如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。
把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。
一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。

classpath

classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。
因为Java是编译型语言,源码文件是.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件。所以,classpath就是一组目录的集合,它设置的搜索路径与操作系统相关.

假设classpath是.;C:\work\project1\bin;C:\shared,当JVM在加载abc.xyz.Hello这个类时,会依次查找:
<当前目录>\abc\xyz\Hello.class
C:\work\project1\bin\abc\xyz\Hello.class
C:\shared\abc\xyz\Hello.class
注意: .代表当前目录
强烈不推荐在系统环境变量中设置classpath,那样会污染整个系统环境。在启动JVM时设置classpath才是推荐的做法。
在IDE中运行Java程序,IDE自动传入的-cp参数是当前工程的bin目录和引入的jar包。

有很多“如何设置classpath”的文章会告诉你把JVM自带的rt.jar放入classpath,但事实上,根本不需要告诉JVM如何去Java核心库查找class,JVM怎么可能笨到连自己的核心库在哪都不知道?不要把任何Java核心库添加到classpath中!JVM根本不依赖classpath加载核心库!更好的做法是,不要设置classpath!默认的当前目录.对于绝大多数情况都够用了。

jar包

如果有很多.class文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。

jar包就是用来干这个事的,它可以把package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。

jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class,就可以把jar包放到classpath中:
java -cp ./hello.jar abc.xyz.Hello
这样JVM会自动在hello.jar文件里去搜索某个类。
那么问题来了:如何创建jar包?
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip改为.jar,一个jar包就创建成功。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF文件,MANIFEST.MF是纯文本,可以指定Main-Class和其它信息。JVM会自动读取这个MANIFEST.MF文件,如果存在Main-Class,我们就不必在命令行指定启动的类名,而是用更方便的命令:
在大型项目中,不可能手动编写MANIFEST.MF文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。

模块

从Java 9开始,JDK又引入了模块(Module)。主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容