Java面向对象(一)

Java OOP简单介绍(一)
 

Java语言是纯粹的面向对象的程序设计语言,这主要表现为Java完全支持面向对象的三种基本特征: 继承、封装、多态。 Java语言完全以对象为中心,Java程序的最小单位是类,整个Java程序由一个一个类组成的。

 
Java完全支持使用对象、类、继承、封装、消息等基本概念来进行程序设计,允许从现实世界中客观存在的事物(即对象)出发来构造软件系统,在系统构造中尽可能运用人类的自然思维方式。
 
面向对象的方式实际上由 OOA(面向对象分析)、OOD(面向对象设计)和OOP(面向对象编程)三个部分有机组成。其中,OOA和OOD的结构需要使用一种方式来描述并记录,目前业界统一采用UML(统一建模语言)来描述并记录OOA和OOD的结果。
 
Java是面向对象的程序设计语言,类是面向对象的重要内容,可以把类当成一种自定义类型,可以使用类来自定义变量,这种类型的变量统称为引用变量。也就是说,所有类都是引用类型。 
 

类和对象

的修饰符: 可以是public , final , abstract, 或者完全省略这三个修饰符。

成员变量的修饰符: 可以省略,也可以是poblic, protected, private, static, final, 其中public, protected, private 最多只能出现其中之一,可以和static、final组合起来修饰成员变量。

格式: [修饰符] 类型 成员变量名 [=默认值]

方法的修饰符: 可以省略,也可以是poblic、protected、private、static、final、abstract, 其中public、protected、private 最多只能出现其中之一;abstract和final最多只能出现其中之一,它们可以和static 组合起来修饰方法。

格式: 

[修饰符] 方法返回值类型 方法名(形参列表)

{

  //由零到多条的可执行语句组成的方法体

}

 

注意:static修饰的方法和成员变量,既可以通过类来调用,也可以通过实例来调用;没有使用static修饰的普通方法和成员变量,只能通过实例来调用。

 

对象,引用和指针

对于代码 Person p = new Person();

这行代码创建了一个Person实例,也称为 Person对象,这个Person对象被赋值给p变量。(它是一个引用变量)

当把这个Person对象赋值给p变量时,系统是如何处理的呢?

  Java让引用变量指向这个对象即可。也就是说,引用变量里面存储的只是一个引用,它指向一个实际的对象。这个引用,它被存储在栈内存里,指向实际的Person对象;而真正的Person对象存储在堆内存中。

Java的引用变量和C语言的指针很像,他们都是存储一个地址,通过这个地址来引用到实际变量。实际上,java里的引用就是C语言里的指针,只是Java语言把这个指针封装起来,避免开发者进行繁琐的指针操作。

堆内存里的对象可以有多个引用,即多个引用变量指向用一个对象。

例如: Person p2 = p;

 

如果堆内存里的对象没有任何变量指向该对象,那么程序将无法再访问该对象,这个变量也就变成了垃圾,Java的垃圾回收机制将回收该对象,释放该对象所占的内存区。

因此,如果希望通知垃圾回收机制回收某个对象,只需要切断该对象的所有引用变量和它之间的关系即可,也就是把这些引用变量赋值为null。

 

对象的this引用

Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:

1. 构造器中引用该构造器正在初始化的对象

2. 在方法中引用调用该方法的对象

this关键字最大的作用是:让类中一个方法,访问类里的另一个方法或实例变量。

 

this可以代表任何对象,当this出现在某个方法体中,它所代表的对象是不确定的,但它的类型是确定的:它所代表的只能是当前类的实例;只有当这个方法被调用时,它所代表的对象才被确定下来,谁在调用这个方法,this就代表谁。

Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。(省略this前缀只是一种假象,虽然程序员省略了调用方法之前的this,但实际上这个this依然是存在的)

大部分时候,一个方法访问该类中定义的其他方法、成员变量时,加不加this前缀的效果是完全一样的。

延伸

对于static修饰的方法而言,则可以使用类来调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this引用。

由于static修饰的方法中不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此java语法规定: 静态成员不能直接访问非静态成员。

 

成员变量和局部变量

成员变量:类变量 和 实例变量

局部变量:形参, 方法局部变量, 代码块局部变量 (除了形参之外,都必须要显示初始化

 

代码块局部变量:只要离开了对应的代码块,这个局部变量就会被销毁。

方法局部变量:  其作用域从定义该变量开始,知道该方法结束。

 

在同一个类中,成员变量的作用域是整个类内有效,一个类中不能定义两个同名的方法局部变量,即使是一个类变量,一个实例变量也不行。

一个方法里,不能定义两个同名的方法局部变量,方法局部变量和形参也不能同名;

同一个方法里,不同代码块中的局部变量可以同名;

如果先定义代码块局部变量,后定义方法局部变量,它们可以同名。

java允许成员变量和局部变量同名,在方法中,成员变量会覆盖成员变量,如果需要在方法中引用被覆盖的成员变量,则可以使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。

 

方法的参数传递机制

Java里方法的参数传递方式只有一种:值传递

注意Java程序运行时,方法栈区的概念。

 

访问控制符

  private default protected public
同一个类中
同一个包中  
子类中    
全局范围内      

default: 包访问权限

在这里需要注意包的概念:

Java引入了import关键字,可以用来向某个java文件中导入指定包层次下某个类或者全部类,import语句应该出现在package语句之后,类定义之前。

JDK1.5以后增加了静态导入的语法。用于导入指定类的某个静态成员变量、方法:

  import static package.subpackage...ClassName.fieldName|methodName;

或导入全部的静态成员变量、方法:

  import static package.subpackage...ClassName.*;

 

封装

将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

为了实现良好的封装,需要从以下两个方面考虑:

1. 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问

2. 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和控制。

 

继承

Java语言使用extends关键字实现继承,具有单继承的特点,每个子类只有一个直接父类。

如果Java类没有显示定义指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此java.lang.Object是所有类的父类。要么是直接父类,要么是其间接父类。

如果子类中包含与父类同名的方法的现象称为 方法重写(Override),或 方法覆盖。需要遵循“两同两小一大”的规则:

  两同:方法名相同,形参列表相同

  两小:子类方法返回值类型应该比父类方法返回值类型更小或者相等;子类方法申明抛出的异常类应该比父类方法申明抛出的异常类要更小或者相等。

  一大:子类方法的访问权限应该比父类方法的访问权限更大或者相等。

父类中private的方法不能被重写,如果子类中有相同的方法,只能算是子类新的方法。

当子类覆盖了父类方法后,子类对象将无法直接访问父类中被覆盖的方法,但可以在子类中调用父类中被覆盖的方法。通过super关键字。

super限定

super是java提供的一个关键字。用来限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中,super也不能出现在static修饰的方法中。

static修饰的方法属于类的,该方法的调用者是一个类,而不是一个对象,因而super也失去了意义。

如果在构造器中使用super,则super用于限定该构造期初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。

如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无须显示使用super或父类名作为调用者。

如果在某个方法中访问名为a的成员变量,但没有显示指定调用者,则系统查找a的顺序为:

  (1) 查找该方法中是否有名为a的局部变量

  (2) 查找当前类中是否包含名为a的成员变量

  (3) 查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能查找名为a的成员变量,则系统会出现编译错误。

如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。

 

在一个构造器中调用另一个重载的构造器,使用this调用来完成,在子类构造器中调用父类构造器,使用super调用来完成。

使用super调用父类构造器,也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。

 

创建任何对象,总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。

 到底何时需要从父类派生出子类呢?

  子类需要额外增加属性,而不仅仅是属性值得改变。

  子类需要增加自己独有的行为方式(包括增加新的方法或者重写父类的方法)

多态

Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态
 1 public class SubClass extends BaseClass {
 2     public String book = "Java书籍";
 3 
 4     public void test() {
 5         System.out.println("子类覆盖父类的方法");
 6     }
 7 
 8     public void sub() {
 9         System.out.println("子类的普通方法");
10     }
11 
12     public static void main(String[] args) {
13         // 下面编译时和运行时类型完全一致,因此不会出现多态
14         BaseClass bc = new BaseClass();
15         System.out.println(bc.book);
16         bc.base();
17         bc.test();
18         SubClass sc = new SubClass();
19         System.out.println(sc.book);
20         sc.sub();
21         sc.test();
22         System.out.println();
23         // 下面编译时和运行时类型不一致,因此出现多态
24         BaseClass polymorphism = new SubClass();
25         // 输出1 -- 表明访问的是父类对象的实例变量
26         System.out.println(polymorphism.book);
27         // 下面执行从父类继承到的base()方法
28         polymorphism.base();
29         // 执行当前类的test()方法
30         polymorphism.test();
31         // 因为polymorphism的编译时类型是BaseClass,没有提供sub()方法,会出现编译错误
32         // polymorphism.sub();
33 
34     }
35 
36 }
37 
38 class BaseClass {
39     public int book = 1;
40 
41     public void base() {
42         System.out.println("父类的普通方法");
43     }
44 
45     public void test() {
46         System.out.println("父类被覆盖的方法");
47     }
48 }

输出结果:

父类被覆盖的方法
Java书籍
子类的普通方法
子类覆盖父类的方法

1
父类的普通方法
子类覆盖父类的方法


 

相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,就是多态(Polymorphism)
 
当把子类对象赋值给父类引用变量时,被称为 向上转型,由系统自动完成。这种转型通常是成功的,这也从侧面证实了子类是一种特殊的父类。 这种转型知识表明这个引用变量的编译时类型是父类,但实际执行他的方法时,依然表现出子类对象的行为方式。
引用变量在编译阶段,只能调用其编译时类型所具备的方法,但运行时则执行它运行时类型的方法。
通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的变量。
 
如以上情况限定,如果需要引用变量调用其运行时类型的方法,只能通过强制类型转换的方式,借助强制类型转换符,即小括号()。
  用法:(type)variable     用于将variable变量转换成type类型的变量。
注意:
  基本类型之间的转换只能在数值类型之间进行。这里的数值类型指的是:整数型、浮点型、字符型。 数值型和布尔型之间不能进行类型转化。
  引用类型之间的转化,只能在具有继承关系的两个类型之间进行。
 1 public class ConversionTest {
 2     public static void main(String[] args) {
 3         double d = 13.4;
 4         long i = (long) d;
 5         System.out.println(i);
 6 
 7         int in = 5;
 8         // Cannot cast from int to boolean
 9         // boolean b = (boolean) in;
10 
11         Object ob = "Hello";
12         // String是Object的子类,可以强转
13         // 而且ob变量的实际类型也是String,运行时也会通过。
14         String s = (String) ob;
15         System.out.println(s);
16 
17         Object obj = new Integer(5);
18         // obj的实际类型是Integer,运行时,下面会引发
19         // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
20         String sObj = (String) obj;
21     }
22 }

为了程序的健壮性,修改第20行代码

1 // 判断是否可以成功转换,避免ClassCastException异常
2         if(obj instanceof String){
3             String sObj = (String) obj;
4         }

 

把一个父类对象赋值给子类引用变量时,就需要强制类型转换,而且还可能在运行时产生ClassCastException异常,使用instanceof 运算符可以让强制类型转换更安全。
 

instanceof 运算符

它的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成是一种特殊的类),它用于判断前面的对象是否是后面的类,或者子类,实现类的实例。如果是,则返回true,否则返回false。
 
注意:instanceof运算符前面操作数的编译类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会出现编译错误。
 

组合

做法:通常是需要在新类里使用private修饰被组合的旧类对象。

组合要表达的是“有(has-a)”的关系;继承要表达的是一种“是(is-a)”的关系。

初始化块:

它也可以对java对象进行初始化操作。
它的修饰符只能是static,使用static修改的初始化块被称为 静态初始化块。也称为类初始化块
初始化只在创建Java对象时隐式执行,而且在执行构造器之前执行。执行顺序:前面定义的先执行,后面定义的后执行。
 1 public class Person {
 2     // 下面定义第一个初始化块
 3     {
 4         int a = 6;
 5         if (a > 5) {
 6             System.out.println("Person的第一个初始化块,局部变量a的值大于5");
 7         }
 8         System.out.println("Person的第一个初始化块");
 9     }
10     // 下面定义第二个初始化块
11     {
12         System.out.println("Person的第二个初始化块");
13     }
14 
15     public Person() {
16         System.out.println("Person的无参构造函数");
17     }
18 
19     public static void main(String[] args) {
20         new Person();
21     }
22 }
 结果为:
Person的第一个初始化块,局部变量a的值大于5
Person的第一个初始化块
Person的第二个初始化块
Person的无参构造函数
当初始化块被static修饰后,这个初始化块就变成了静态初始化块
 1 public class Test {
 2     public static void main(String[] args) {
 3         new Leaf();
 4         new Leaf();
 5     }
 6 }
 7 
 8 class Root {
 9     static {
10         System.out.println("Root的静态初始化块");
11     }
12     {
13         System.out.println("Root的普通初始化块");
14     }
15 
16     public Root() {
17         System.out.println("Root的无参构造函数");
18     }
19 }
20 
21 class Mid extends Root {
22     static {
23         System.out.println("Mid的静态初始化块");
24     }
25     {
26         System.out.println("Mid的普通初始化块");
27     }
28 
29     public Mid() {
30         System.out.println("Mid的无参构造函数");
31     }
32 
33     public Mid(String msg) {
34         // 通过this调用同一类中重载的构造器
35         this();
36         System.out.println("Mid的带参构造函数,参数是: " + msg);
37     }
38 }
39 
40 class Leaf extends Mid {
41     static {
42         System.out.println("Leaf的静态初始化块");
43     }
44     {
45         System.out.println("Leaf的普通初始化块");
46     }
47 
48     public Leaf() {
49         super("测试");
50         System.out.println("Leaf的无参构造函数");
51     }
52 }

结果为:

Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,参数是: 测试
Leaf的普通初始化块
Leaf的无参构造函数
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,参数是: 测试
Leaf的普通初始化块
Leaf的无参构造函数

 结果分析:

  第一次创建一个Leaf对象时,因为系统中还不存在Leaf类,因此需要先加载再初始化Leaf类,初始化Leaf类时,会先执行其顶层父类的静态初始化块,再执行其直接父类的静态初始化块,最后才执行本身的静态初始化块。

  一旦Leaf类初始化成功后,Leaf类将在虚拟机中一直存在,因此第二次创建Leaf实例时无须再次对Leaf类进行初始化。

  普通初始化块和构造器的执行顺序,每次创建Leaf对象时,都需要先先执行最顶层父类的初始化块、最顶层父类的构造器,然后执行父类的初始化块、构造器.....直到执行当前类的初始化块、当前类的构造器。

执行顺序总结: 
类初始化阶段。先执行最顶层父类的静态初始化块,然后依次向下,直到执行当前类的静态初始化块。
对象初始化阶段。先执行最顶层父类的初始化块、最顶层父类的构造器,然后依次向下,直到执行当前类的初始化块、当前类的构造器。
 
posted @ 2017-01-22 11:25  无涯的风  阅读(403)  评论(0编辑  收藏  举报