Java Basic One

对象的三大特征

对象的三种基本特征:继承、封装、多态。

Java 语言为纯粹的面向对象的程序编程语言,主要表现为 Java 完全支持对象的三大基本特征。

封装: 将对象的实现细节隐藏起来,然后通过一些公用方法暴露该对象的功能。

继承:子类继承父类,子类作为一种特殊的父类,将直接获得父类的属性和方法。

多态:子类对象可以直接赋给父类变量,但在运行时依然表现出子类的行为特征,这意味着同一个类型的引用对象在执行同一个方法时,可能表现出多种行为特征。

1
2
3
4
5
6
7
8
// B 继承 A
A a;
a = new A();
a.testMethod();// 执行 A 中方法
a = new B();
a.testMethod();// 执行 B 中方法

//在此例中,同一个类型引用执行同一个方法,可能表现多种行为特征:类型为 A ,该类的对象 a 执行 testMethod() 方法会根据其引用对象的具体类型表现出不同的行为特征 -- A#testMethod()或B#testMethod()。

Java 是静态的强类型的语言

Java 是静态的,一旦一个类被定义,如果对这个类有所更改,只要不重新编译这个类,那么这个类以及这个类拥有的成员变量就不会发生改变。

Java 的强类型主要表现在两方面:

  1. 所有的变量必须先声明后使用;
  2. 指定类型的变量只能赋相同类型的值;

基于上面,Java 在编译期就会确定成员变量的类型。

变量

编程的本质就是对内存中的数据访问和修改,程序所用到的数据都会保存在内存中,程序员需要通过一种机制来访问和修改内存中的数据,这种机制就是 变量 ,每个变量代表某一小块内存。变量是有名字的,程序对变量 赋值,就是把数据装入该变量所代表的内存区的过程;程序 读取变量,就是从变量代表的内存区取值的过程。可以简单的理解:变量相当于一个有名称的容器,该容器用于装各种不同类型的数据。

对象和引用

对象与引用

1
Person p = new Person("Mike",20)

由上文中变量是需要是需要内存存储数据的,此语句中 new Person("Mike",20) 只是在堆内存中开辟出一块内存去存储这个对象,而 Person p 则需要在栈内存中开辟一块内存来存储变量以及相关内容,至于在此区域存储的到底是什么,需要在赋值后才会具体化。在此例中,赋值语句后,变量 p 对应的栈内存存储的为新建对象 – new Person 的内存地址。

this 引用

首先 this 代表一定是个对象,其次 this 出现在不同位置含义有所不同:

  • 在构造器中代表正在初始化的对象
  • 在方法中代码调用该方法的对象

因此 static 方法中不能出现 this,因为 static 的方法属于类,是通过类去调用,此时 this 代表了类,违背了 this 代表的为一个对象的前提。

同时 static 方法不可调用非 static 变量和方法也是基于这个原因,因为类中的变量和方法存在隐式的 this 引用,即为 this 作为调用者必须是一个对象。

方法重载

方法名相同,而参数列表不同的现象称为方法重载。

  • 为什么返回值不作为方法重载的区分标准?

Java 调用方法时可以忽略返回值,如果将返回值作为重载的标准,那么存在以下情况:

1
2
3
4
5
6
int method();
void method()

void test(){
method();
}

此时 Java 系统将不能分辨出 test 方法中调用的 method() 是具体哪一个方法。

变量及其运行机制

成员变量:

  1. 类变量:从类的准备阶段开始存在,到系统完全销毁这个类。
  2. 实例变量:从实例的创建始存在,到系统完全销毁这个实例。

局部变量:

  1. 形参
  2. 方法内局部变量
  3. 代码块中局部变量

局部变量只要离开了相应的代码块(方法、代码块),局部变量就会被销毁。

成员变量的初始化和内存中的运行机制

JVM 加载类经历以下几个阶段:

  1. 类加载
  2. 类验证
  3. 类准备
  4. 类解析
  5. 类初始化

当系统加载类或创建类实例时,系统会自动为成员变量分配内存,并赋初值。
Person 类如下:

1
2
3
4
5
6
7
8
9
class Person{
private static int phone;
private String name;
}

// Person 类使用
Person a = new Person();
a.phone = 110;
a.name = "mike";

当执行 Person a = new Person(); 时,如果程序中第一次使用 Person 类, JVM 将会加载 Person 类,并初始化这个类。在类的准备阶段,JVM 将会该类的类变量分配内存空间并初始化。当 Person 类初始化完成后,系统中的内存情况如下图:

类初始化

当类完成初始化后,JVM 将在 堆内存 中为 Person 类分配一块内存(JVM 会为Person类创建一个对象),这块内存区包含了类变量 phone 的内存,并初始化它的初值。

接着,系统创建一个 Person 对象,并把这个对象赋值给 p 变量, Person 对象中包含了实例变量–name,实例变量在创建实例时分配内存空间并赋初值。JVM 创建了第一个 Person 对象后,内存分配情况如下:

实例变量初始化

当再次创建 Person 对象时,不需要再对 Person 类初始化。

对类变量和实例变量赋值时,内存分配情况如下:

赋值

其中,类变量为所有实例共有,每个实例都有权对其进行更改。

局部变量的初始化和内存中的运行机制

系统不会为局部变量进行初始化,所以局部变量必须手动初始化。

与成员变量不同,局部变量的分配内存为所在的方法栈区,如果局部变量时基本数据类型,会直接存在方法栈中;如果为引用变量,则存放引用地址,通过该引用地址指向实际引用的对象。

1
2
3
void test(int a,Person p){
....
}

test 方法栈中内存分配情况如下:

局部变量

访问权限

  • private:类私有
  • default:同包可访问
  • protected:同包及子类可访问
  • public:所有类可访问

构造器与成员变量初始化

我们知道在创建一个对象时,系统会为这个对象的实例变量进行默认初始化,根据数据类型的不同初始值为:0、falase、null。如果想要改变默认初始化,想让系统创建对象时就位对象的实例变量显示指定初始值,可以通过构造器来实现。

封装

继承

继承关系中,子类和父类拥有相同的实例变量时的内存模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent {
public String tag = "parent";
...
}

class Son extends Parent{
private String tag = "son";
}


Son son = new Son();
// sout(son.tag); 编译错误,不可访问私有变量
sout(((Parent)son).tag); // 打印日志为: parent

图

子类的实例变量隐藏父类实例变量

多态

Java 引用变量有两个类型:编译时类型运行时类型。编译时类型是由声明该变量时使用的类型决定,运行时类型是由实际赋给该变量的对象决定。

当编译时类型和运行时类型不一致,就可能发生所谓的 多态

引用变量在编译时只能调用编译时类型所拥有的方法,运行时在可以执行其运行时类型所具有的方法。因此,在编写 Java 代码是,引用变量只能调用声明该变量时所用的类(编译时类型)中的方法,例如 Object person = new Person(); ,person 只能调用 Object 类中的方法,不能调用 Person 中的方法。

如果想要编译时类型调用运行时类型的方法,可以对引用变量进行强转型。

继承与组合

继承是实现类复用的重要手段,但是继承带来了一个坏处:破坏了封装性,即子类可以任意的访问父类的实例变量、重新父类的方法。

为了保证父类良好的封装性不被子类任意更改,设计父类应遵循如下规则:

  1. 尽量隐藏父类的内部数据,使用 private 修饰;
  2. 不要让子类任意访问和修改方法。辅助方法使用 private 修饰;父类中可以被外界访问但是不可以被子类重新的方法使用 public final 修饰;希望被子类重写的方法使用 protected 修饰。
  3. 尽量不要在父构造器中调用被子类重写的方法。

针对第 3 条,特殊说明一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
public A(){
test();
}

public void test(){
sout("A");
}
}

class B extends A{
private String des;
public void test(){
sout("B" + des.length());
}
}

//调用
B b = new B();

系统在创建 B 实例时,会首先执行父类的构造器,在父构造器中调用了被父类重写的方法,此时执行的方法为子类的重写后的方法。此时 B 的实例变量 des 还没有初始化为 null,调用会发生空指针异常。

产生此现象的原因我猜大致是因为这样:

这个问题又牵涉到 this 引用的问题,在这个问题中我们很明确的表明当 this 出现在构造器中表示正在实例化的对象, A 构造器中 test() 方法的调用中隐藏了默认的 this 引用,所以此时 this 代表被实例化的 B 的对象,所以 A 中 test() 执行 B 中的 test() 方法。