面向对象基础

没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删

引言

兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来。所以奉劝大家,面向对象还是要掌握一门,虽然Python好写舒服,但是毕竟不能完全面向对象,也没有那么多的应用场景,所以,奉劝看到本文的各位,还是提前学好C#或者Java。

class和instance

  • 所以,只要理解了class和instance的概念,基本上就明白了什么是面向对象编程。
  • class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型。
  • 而instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同

定义class

  • 在Java中,创建一个类,例如,给这个类命名为Person,就是定义一个class
class Person {
public String name;
public int age;
}

创建实例

Person ming = new Person();
  • 定义了class,只是定义了对象模版,而要根据对象模版创建出真正的对象实例,必须用new操作符。

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

  • 上述代码创建了一个Person类型的实例,并通过变量ming指向它。

  • 注意区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。

  • 有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问实例变量可以用变量.字段

ming.name = "Xiao Ming"; // 对字段name赋值

创建两个Person的实例,并用Person类型的变量ming、hong指向他们。

            ┌──────────────────┐
ming ──────>│Person instance │
├──────────────────┤
│name = "Xiao Ming"│
│age = 12 │
└──────────────────┘
┌──────────────────┐
hong ──────>│Person instance │
├──────────────────┤
│name = "Xiao Hong"│
│age = 15 │
└──────────────────┘

方法

  • 虽然外部代码不能直接修改private字段,但是,外部代码可以调用方法setName()setAge()来间接修改private字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。
  • 一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。

定义方法

  • 从上面的代码可以看出,定义方法的语法是:
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
  • 方法返回值通过return语句实现,如果没有返回值,返回类型设置为void,可以省略return

private方法

public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setBirth(2008);
System.out.println(ming.getAge());//调用getAge()的时候不关心有没有age字段
}
} class Person {
private String name;
private int birth; public void setBirth(int birth) {
this.birth = birth;
} public int getAge() {
return calcAge(2019); // 调用private方法
} // private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}
  • 外部代码无法调用,但是类内部代码可以调用
  • 方法可以封装一个类的对外接口,调用方不需要知道也不关心Person实例在内部到底有没有age字段。

this变量

  • 在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。

  • 如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this

class Person {
private String name; public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}

方法参数

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

可变参数

  • 可变参数用类型...定义,可变参数相当于数组类型:
class Group {
private String[] names; public void setNames(String... names) {
// 完全可以吧可变参数当做数组来写
// 比如这里可以写成 String[]
this.names = names;
}
}
  • 但是,调用方需要自己先构造String[],比较麻烦。例如:
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1String[]
  • 另一个问题是,调用方可以传入null
Group g = new Group();
g.setNames(null);
  • 而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null

参数绑定

  • 调用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
  • 修改外部的局部变量n,不影响实例page字段,原因是setAge()方法获得的参数,复制了n的值
  • 基本类型参数的传递,是调用方值的复制。
  • 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。

构造方法

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 Person {
public Person() {
}
}
  • 如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法

  • 如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来

  • 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false

  • 也可以对字段直接进行初始化:

class Person {
private String name = "Unamed";
private int age = 10;
}
  • 在Java中,创建对象实例的时候,按照如下顺序进行初始化:
  1. 先初始化字段,例如,int age = 10;表示字段初始化为10double salary;表示字段默认初始化为0String name;表示引用类型字段默认初始化为null
  2. 执行构造方法的代码进行初始化。
  • 因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)的字段值最终由构造方法的代码确定。

多构造方法

  • 可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分。

  • 一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)

class Person {
private String name;
private int age; public Person(String name, int age) {
this.name = name;
this.age = age;
} public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
} public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}

方法重载

  • 在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
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 + "!");
}
}
}
  • 方法名相同,但各自的参数不同,称为方法重载(Overload

  • 注意:方法重载的返回值类型通常都是相同的。

  • 方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。

    举个例子,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关键字来实现继承:
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) { … }
}
  • 在OOP的术语中,我们把Person称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。

继承树

  • 定义Person时没有写 extends
  • 在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。
┌───────────┐
│ Object │
└───────────┘


┌───────────┐
│ Person │
└───────────┘


┌───────────┐
│ Student │
└───────────┘
  • Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。

Protected

  • 继承有个特点,就是子类无法访问父类的private字段或者private方法,但是用protected修饰的字段可以被子类访问。

  • 因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问。

Super

  • super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName

  • 如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

  • 这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

向上转型

  • 把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)
Person p = new Student();// upcasting

向下转型

  • 把一个父类类型强制转型为子类类型,就是向下转型(downcasting)

  • 不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

  • Java中的向下转型很容易失败,出现ClassCastException

  • 为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true Student n = null;
System.out.println(n instanceof Student); // false
  • instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false

  • 利用instanceof,在向下转型前可以先判断:

Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}

区分继承和组合

  • 因为StudentPerson的一种,它们是is关系。而StudentBook的关系是has关系。

  • 具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:

class Student extends Person {
// 在类中有一个实例
// 从结果上来说,就是没创建一个Student实例就会有一个book实例作为其字段
protected Book book;
protected int score;
}

多态

  • 在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)

  • Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override

  • 注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。

  • 加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。// 意思是不加上@overwrite也没有问题,但是@overwrite是帮助检查的

public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run(); // 应该打印Person.run还是Student.run?
// 答案是 Student.run
// 因为 Java的实例方法调用的是基于运行时的实际类型的调用
}
} class Person {
public void run() {
System.out.println("Person.run");
}
} class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
  • Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

  • 这个非常重要的特性在面向对象编程中称之为多态。Polymorphic。

  • 多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期 实际类型的方法。

  • 多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

    // 创建Income类型的实例数组
Income[] incomes = new Income[] {
new Income(3000),
new Salary(7500),
new StateCouncilSpecialAllowance(15000)
};

覆写Object方法

  • 因为所有的class最终都继承自Object,而Object定义了几个重要的方法:

    • toString():把instance输出为String
    • equals():判断两个instance是否逻辑相等;
    • hashCode():计算一个instance的哈希值。

调用super

  • 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
} Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}

final

  • 如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为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 class Person {
protected String name;
} // compile error: 不允许继承自Person
Student extends Person {
}
  • 一个类的实例字段,同样可以用final修饰,用final修饰的字段在初始化后不能被修改,但是可以在构造方法中初始化final字段,←这种方法非常常用,保证实例一旦创建,就不会再被修改。
class Person {
// 将类的实例字段用final修饰
public final String name;
// 在构造方法中初始化final修饰的字段
public Person(String name) {
this.name = name;
}
}
  • 重写(Overload)是不准许父类和子类只是返回值类型不同,而覆盖(Override)是准许父类和子类的返回值类型不一样,只不过那个返回值类型要是一种继承关系,比如文章中的process()方法返回值一个是Grain类型,一个是Wheat类型,Grain和Wheat有继承关系,所以可以,而 void类型和int类型不存在这样的关系,所以不行。

抽象类

  • 如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,则可以把父类的方法声明为抽象方法,声明抽象方法直接在方法前面加上abstract即可,又因为包含抽象方法,所以也必须将类声明为抽象类,这样就可以正常编译了,但是记住,这个抽象类还是无法实例化
abstract class Person {
public abstract void run();
} Person p = new Person(); // 编译错误
  • 无法实例化的抽象类有什么用?因为抽象类本身被设计成只能用于继承,实际上可以将抽象类当做是一种继承规范,子类在继承抽象类的时候必须Override父类的抽象方法
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run();
}
} abstract class Person {
public abstract void run();
} class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}

面向抽象编程

  • 定义了抽象类,以及具体继承了抽象类的子类,我们就可以通过抽象类去引用具体类型的子类实例,这种抽象类的好处在于,我们不关心抽象类变量的具体类型,只关心去引用具体类型的子类实例,避免引用实际类型的方式,称之为面向抽象编程。
  • 面向抽象编程的本质就是:
    • 上层代码只定义规范(例如:abstract class Person);
    • 不需要子类就可以实现业务逻辑(正常编译);
    • 具体的业务逻辑由不同的子类实现,调用者并不关心。

接口

  • 如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该类改写为接口。
  • 在Java中用interface来声明接口。
  • 因为接口定义的所有方法都是public abstract所以可以省略public abstract
interface Person {
void run();
String getName();
}
  • 当一个具体的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
...
}

术语

  • Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。

  • 抽象类和接口的对比如下:

abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法

接口继承

  • 一个interface可以继承自另一个interface,此时用extends声明,相当于扩展了接口。
interface Hello {
void hello();
} interface Person extends Hello {
void run();
String getName();
}

继承关系

  • 一般来说,公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度。
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
  • 在使用时,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象
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方法无法访问字段,而抽象类的普通方法可以访问实例字段。

静态字段和静态方法

  • 在一个类中定义一个字段,我们称之为实例字段,每个实例的字段虽然“名字”都一样,但其实是独立的,互不影响的。实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。静态字段用static修饰。

  • 对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例,而是属于类。

  • 推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段(非实例字段)

public class Main {
public static void main(String[] args) {
Person ming = new Person("Xiao Ming", 12);
Person hong = new Person("Xiao Hong", 15);
ming.number = 88;
System.out.println(hong.number);
hong.number = 99;
System.out.println(ming.number);
}
} class Person {
public String name;
public int age; public static int number; public Person(String name, int age) {
this.name = name;
this.age = age;
} }
// 虽然实例可以访问静态字段,但是他们指向的都是Person Class的静态字段。
┌──────────────────┐
ming ──>│Person instance │
├──────────────────┤
│name = "Xiao Ming"│
│age = 12 │
│number ───────────┼──┐ ┌─────────────┐
└──────────────────┘ │ │Person class │
│ ├─────────────┤
├───>│number = 99 │
┌──────────────────┐ │ └─────────────┘
hong ──>│Person instance │ │
├──────────────────┤ │
│name = "Xiao Hong"│ │
│age = 15 │ │
│number ───────────┼──┘
└──────────────────┘

静态方法

  • static修饰的方法称为静态方法。

  • 调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。

  • 因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。

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;
} }
  • 静态方法经常用于工具类。例如:

    • Arrays.sort()

    • Math.random()

  • 静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。

接口的静态字段

  • 因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的。

  • 并且interface中的静态字段必须为final类型。

public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
  • 实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为,编译器会自动把该字段变为public static final类型。
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}

  • 在Java中,我们使用package来解决名字冲突

  • 一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名

  • 举例说明

    • 小明的Person类存放在包ming下面,因此,完整类名是ming.Person
    • 小红的Person类存放在包hong下面,因此,完整类名是hong.Person
    • 小军的Arrays类存放在包mr.jun下面,因此,完整类名是mr.jun.Arrays
    • JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays
    • 在定义class的时候,我们需要在第一行声明这个class属于哪个包。
  • 小明的Person.java文件:

package ming; // 申明包名ming

public class Person {
}
  • 小军的Arrays.java文件:
package mr.jun; // 申明包名mr.jun

public class Arrays {
}
  • 在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。

  • 包可以是多层结构,用.隔开。例如:java.util

要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。

  • 我们还需要按照包结构把上面的Java文件组织起来。
package_sample
└─ src
├─ hong
│ └─ Person.java
│ ming
│ └─ Person.java
└─ mr
└─ jun
└─ Arrays.java
  • 编译后的.class文件也需要按照包结构存放。如果使用IDE,把编译后的.class文件放到bin目录下,那么,编译的文件结构就是:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class

包作用域

  • 不用publicprotectedprivate修饰的字段和方法就是包作用域,其实默认是protected作用域。
  • 类添加了public属性是可以对外包开放;没有修饰就是默认protected,就是仅对包内使用。
package hello;

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

import

  • 在一个class中,我们总会引用其他的class。用import语句,导入小军的Arrays然后就可以用小军包中的类了。

  • 在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class)。

    • 我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。
  • 还有一种import static的语法,它可以导入一个类的静态字段和静态方法,但是它很少使用。

  • Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:

    • 如果是完整类名,就直接根据完整类名查找这个class

    • 如果是简单类名,按下面的顺序依次查找:

      • 查找当前package是否存在这个class
      • 查找import的包是否包含这个class
      • 查找java.lang包是否包含这个class
    • 如果按照上面的规则还无法确定类名,则编译报错。

  • 因此,从上面的例子可以看出,编写class的时候,编译器会自动帮我们做两个import动作:

    • 默认自动import当前package的其他class

    • 默认自动import java.lang.*

如果有两个class名称相同,例如,mr.jun.Arraysjava.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

最佳实践

  • 为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

    • org.apache

    • org.apache.commons.log

    • com.liaoxuefeng.sample

  • 子包就可以根据功能自行命名。

作用域

public

  • 定义为publicclassinterface可以被其他任何类访问:

  • 定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限:

private

  • 实际上,确切地说,private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法:

  • 由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:

public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
} // private方法:
private static void hello() {
System.out.println("private hello!");
} // 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
} }

protected

  • protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类

package

  • 最后,包作用域是指一个类允许访问同一个package的没有publicprivate修饰的class,以及没有publicprotectedprivate修饰的字段和方法。

包没有父子关系,com.apachecom.apache.abc是不同的包。

局部变量

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

public class Hello {
void hi(String name) { // ①
String s = name.toLowerCase(); // ②
int len = s.length(); // ③
if (len < 10) { // ④
int p = 10 - len; // ⑤
for (int i=0; i<10; i++) { // ⑥
System.out.println(); // ⑦
} // ⑧
} // ⑨
} // ⑩
}
  • 我们观察上面的hi()方法代码:

    • 方法参数name是局部变量,它的作用域是整个方法,即①~⑩;

    • 变量s的作用域是定义处到方法结束,即②~⑩;

    • 变量len的作用域是定义处到方法结束,即③~⑩;

    • 变量p的作用域是定义处到if块结束,即⑤~⑨;

    • 变量i的作用域是for循环,即⑥~⑧。

使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。

final

  • final与访问权限不冲突。

  • final修饰class可以阻止被继承

  • final修饰method可以阻止被子类覆写

  • final修饰field可以阻止被重新赋值

  • final修饰局部变量可以阻止被重新赋值

最佳实践

  • 一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。

  • 如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。

  • 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。

classpath和jar

  • classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class

  • 因为Java是编译型语言,源码文件是.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件。

  • 所以,classpath就是一组目录的集合,它设置的搜索路径与操作系统相关。例如,在Windows系统上,用;分隔,带空格的目录用""括起来,可能长这样:

C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
  • 在Linux系统上,用:分隔,可能长这样:
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
  • 现在我们假设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

  • 注意到.代表当前目录。如果JVM在某个路径下找到了对应的class文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错。

  • classpath的设定方法有两种:

    • 在系统环境变量中设置classpath环境变量,不推荐;
    • 在启动JVM时设置classpath变量,推荐。
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello // -classpath可以缩写为-cp
java abc.xyz.Hello // 没有传入-cp参数就只在当前目录搜索

不要把任何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包就创建成功。
  • 假设编译输出的目录结构是这样:

package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
  • 这里需要特别注意的是,jar包里的第一层目录,不能是bin,而应该是hongmingmr

  • 在大型项目中,不可能手动编写MANIFEST.MF文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。

模块

  • 从Java 9开始,JDK又引入了模块(Module)。

  • jar只是用于存放class的容器,它并不关心class之间的依赖。

  • 从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块

  • 把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。

编写模块

  • 编写好的模块在实际运行中其实还不能用,我们需要用它来打包JRE。

运行模块

打包JRE

  • 要分发我们自己的Java应用程序,只需要把这个jre目录打个包给对方发过去,对方直接运行上述命令即可,既不用下载安装JDK,也不用知道如何配置我们自己的模块,极大地方便了分发和部署。

访问权限

  • Java的class访问权限分为public、protected、private和默认的包访问权限。引入模块后,这些访问权限的规则就要稍微做些调整。

  • class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。

  • 举个例子:我们编写的模块hello.world用到了模块java.xml的一个类javax.xml.XMLConstants,我们之所以能直接使用这个类,是因为模块java.xmlmodule-info.java中声明了若干导出:

module java.xml {
exports java.xml; // 导出 java.xml 才能使用这个模块
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}

到头来还是逃不开Java - Java13面向对象基础的更多相关文章

  1. 到头来还是逃不开Java - Java13程序基础

    java程序基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来. ...

  2. 到头来还是逃不开Java - Java13核心类

    Java13核心类 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来 ...

  3. (Java)《head first java》值得Java或面向对象基础的新手看。

    看完这本书后本人收获良多. 内容对Java以及面向对象的入门者非常友好. 抽象的内容惯用图解的方法来剖析,通俗易懂 之前看C#入门经典的面向对象时,依然浓浓的一头雾水. (1)很不解为何实例化要写成A ...

  4. Java项目案例之---开灯(面向对象复习)

    开灯(面向对象复习) 设计一个台灯类(Lamp)其中台灯有灯泡类(Buble)这个属性,还有开灯(on)这个方法 设计一个灯泡类(Buble),灯泡类有发亮的方法 其中有红灯泡类(RedBuble)和 ...

  5. Java与面向对象

    一.面向过程的思想和面向对象的思想 面向对象和面向过程的思想有着本质上的区别, 作为面向对象的思维来说,当你拿到一个问题时,你分析这个问题不再是第一步先做什么,第二步再做什么,这是面向过程的思维,你应 ...

  6. 使用Java实现面向对象编程

    使用Java实现面向对象编程 源码展示: package cdjj.s2t075.com; import java.util.Scanner; public class Door { /* * Doo ...

  7. JAVA的面向对象编程

    JAVA的面向对象编程 面向对象主要针对面向过程. 面向过程的基本单元是函数. 什么是对象:EVERYTHING IS OBJECT(万物皆对象) 全部的事物都有两个方面: 有什么(属性):用来描写叙 ...

  8. java复习面向对象(二)

    java复习面向对象(二) 1.static关键字 举例图片 静态变量 使用static修饰的成员变量是静态变量 如果一个成员变量使用了关键字static,那么这个变量不属于对象自己,而属于所在的类多 ...

  9. java基础学习05(面向对象基础01)

    面向对象基础01 1.理解面向对象的概念 2.掌握类与对象的概念3.掌握类的封装性4.掌握类构造方法的使用 实现的目标 1.类与对象的关系.定义.使用 2.对象的创建格式,可以创建多个对象3.对象的内 ...

随机推荐

  1. ubuuntu截图

    方法1: 按 print screen sysrq 方法2: 系统设置 选择键盘 选择快捷键窗口 选择截图 按照自己的习惯更改快捷键即可.

  2. C/S编程

    https://blog.csdn.net/antony1776/article/details/73717666 实现C/S程序,加上登录注册聊天等功能. 然后要做个协议的样子出来. 比如说注册功能 ...

  3. RestTemplate-记录

    org.springframework.web.client.RestTemplate 1.从使用功能上看,是一种简化请求响应的工具类,从发送请求,到对返回的结果进行json解析.格式不对会有异常.

  4. Hadoop架构: 关于Recovery (Lease Recovery , Block Recovery, PipeLine Recovery)

    该系列总览: Hadoop3.1.1架构体系——设计原理阐述与Client源码图文详解 : 总览 在HDFS中,有三种Recovery 1.Lease Recovery 2.Block Recover ...

  5. oracle sys可以登录,system权限不足,解决方法

    今天在自己电脑上安装了oracle 11g,安装成功后发现 sys 可以正常登录.system 无法登录,显示 ORA-01031: insufficient privileges(权限不足) sel ...

  6. Java补强转

    /* 对于byte/short/char三种类型来说,如果右侧赋值的数值没有超过范围, 那么javac编译器将会自动隐含地为我们补上一个(byte)(short)(char). 1. 如果没有超过左侧 ...

  7. 虚拟机中安装centos7后无法上网,使用桥接网络+ssh

    首先是桥接网络解决无法上网的问题: 1保证你Vmware里面的虚拟机是关机状态2右键点击电脑屏幕右下角小电脑图标,选择打开网络与共享中心,然后点击弹出来的窗口左上角的“更改适配器设置”.这里指的是你W ...

  8. ASP.NET Core搭建多层网站架构【5-网站数据库实体设计及映射配置】

    2020/01/29, ASP.NET Core 3.1, VS2019, EntityFrameworkCore 3.1.1, Microsoft.Extensions.Logging.Consol ...

  9. ASP.NET Core搭建多层网站架构【11-WebApi统一处理返回值、异常】

    2020/02/01, ASP.NET Core 3.1, VS2019 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构[11-WebApi统一处理返回值.异常] 使用I ...

  10. VMware15下载、安装、激活

    1.VMware15下载 链接:https://pan.baidu.com/s/1bI8LReRY-5k81O3rrNgg-A  提取码:6c03 2.VMware15安装 3.VMware15激活