到头来还是逃不开Java - Java13面向对象基础
面向对象基础
没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删
引言
兜兜转转到了大四,学过了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
,不影响实例p
的age
字段,原因是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中,创建对象实例的时候,按照如下顺序进行初始化:
- 先初始化字段,例如,
int age = 10;
表示字段初始化为10
,double salary;
表示字段默认初始化为0
,String name;
表示引用类型字段默认初始化为null
; - 执行构造方法的代码进行初始化。
- 因此,构造方法的代码由于后运行,所以,
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; // 一定会成功
}
区分继承和组合
因为
Student
是Person
的一种,它们是is关系。而Student
和Book
的关系是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
包作用域
- 不用
public
、protected
、private
修饰的字段和方法就是包作用域,其实默认是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.Arrays
和java.util.Arrays
,那么只能import
其中一个,另一个必须写完整类名。
最佳实践
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:
org.apache
org.apache.commons.log
com.liaoxuefeng.sample
子包就可以根据功能自行命名。
作用域
public
定义为
public
的class
、interface
可以被其他任何类访问:定义为
public
的field
、method
可以被其他类访问,前提是首先有访问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
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法。
包没有父子关系,
com.apache
和com.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包就创建成功。
- 因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从
假设编译输出的目录结构是这样:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
这里需要特别注意的是,jar包里的第一层目录,不能是
bin
,而应该是hong
、ming
、mr
。在大型项目中,不可能手动编写
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.xml
的module-info.java
中声明了若干导出:
module java.xml {
exports java.xml; // 导出 java.xml 才能使用这个模块
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
到头来还是逃不开Java - Java13面向对象基础的更多相关文章
- 到头来还是逃不开Java - Java13程序基础
java程序基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来. ...
- 到头来还是逃不开Java - Java13核心类
Java13核心类 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来 ...
- (Java)《head first java》值得Java或面向对象基础的新手看。
看完这本书后本人收获良多. 内容对Java以及面向对象的入门者非常友好. 抽象的内容惯用图解的方法来剖析,通俗易懂 之前看C#入门经典的面向对象时,依然浓浓的一头雾水. (1)很不解为何实例化要写成A ...
- Java项目案例之---开灯(面向对象复习)
开灯(面向对象复习) 设计一个台灯类(Lamp)其中台灯有灯泡类(Buble)这个属性,还有开灯(on)这个方法 设计一个灯泡类(Buble),灯泡类有发亮的方法 其中有红灯泡类(RedBuble)和 ...
- Java与面向对象
一.面向过程的思想和面向对象的思想 面向对象和面向过程的思想有着本质上的区别, 作为面向对象的思维来说,当你拿到一个问题时,你分析这个问题不再是第一步先做什么,第二步再做什么,这是面向过程的思维,你应 ...
- 使用Java实现面向对象编程
使用Java实现面向对象编程 源码展示: package cdjj.s2t075.com; import java.util.Scanner; public class Door { /* * Doo ...
- JAVA的面向对象编程
JAVA的面向对象编程 面向对象主要针对面向过程. 面向过程的基本单元是函数. 什么是对象:EVERYTHING IS OBJECT(万物皆对象) 全部的事物都有两个方面: 有什么(属性):用来描写叙 ...
- java复习面向对象(二)
java复习面向对象(二) 1.static关键字 举例图片 静态变量 使用static修饰的成员变量是静态变量 如果一个成员变量使用了关键字static,那么这个变量不属于对象自己,而属于所在的类多 ...
- java基础学习05(面向对象基础01)
面向对象基础01 1.理解面向对象的概念 2.掌握类与对象的概念3.掌握类的封装性4.掌握类构造方法的使用 实现的目标 1.类与对象的关系.定义.使用 2.对象的创建格式,可以创建多个对象3.对象的内 ...
随机推荐
- ubuuntu截图
方法1: 按 print screen sysrq 方法2: 系统设置 选择键盘 选择快捷键窗口 选择截图 按照自己的习惯更改快捷键即可.
- C/S编程
https://blog.csdn.net/antony1776/article/details/73717666 实现C/S程序,加上登录注册聊天等功能. 然后要做个协议的样子出来. 比如说注册功能 ...
- RestTemplate-记录
org.springframework.web.client.RestTemplate 1.从使用功能上看,是一种简化请求响应的工具类,从发送请求,到对返回的结果进行json解析.格式不对会有异常.
- Hadoop架构: 关于Recovery (Lease Recovery , Block Recovery, PipeLine Recovery)
该系列总览: Hadoop3.1.1架构体系——设计原理阐述与Client源码图文详解 : 总览 在HDFS中,有三种Recovery 1.Lease Recovery 2.Block Recover ...
- oracle sys可以登录,system权限不足,解决方法
今天在自己电脑上安装了oracle 11g,安装成功后发现 sys 可以正常登录.system 无法登录,显示 ORA-01031: insufficient privileges(权限不足) sel ...
- Java补强转
/* 对于byte/short/char三种类型来说,如果右侧赋值的数值没有超过范围, 那么javac编译器将会自动隐含地为我们补上一个(byte)(short)(char). 1. 如果没有超过左侧 ...
- 虚拟机中安装centos7后无法上网,使用桥接网络+ssh
首先是桥接网络解决无法上网的问题: 1保证你Vmware里面的虚拟机是关机状态2右键点击电脑屏幕右下角小电脑图标,选择打开网络与共享中心,然后点击弹出来的窗口左上角的“更改适配器设置”.这里指的是你W ...
- ASP.NET Core搭建多层网站架构【5-网站数据库实体设计及映射配置】
2020/01/29, ASP.NET Core 3.1, VS2019, EntityFrameworkCore 3.1.1, Microsoft.Extensions.Logging.Consol ...
- ASP.NET Core搭建多层网站架构【11-WebApi统一处理返回值、异常】
2020/02/01, ASP.NET Core 3.1, VS2019 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构[11-WebApi统一处理返回值.异常] 使用I ...
- VMware15下载、安装、激活
1.VMware15下载 链接:https://pan.baidu.com/s/1bI8LReRY-5k81O3rrNgg-A 提取码:6c03 2.VMware15安装 3.VMware15激活