JAVA进阶 深入理解面向对象

Java
266
0
0
2023-06-08

JAVA进阶 深入理解面向对象


一、结构化程序设计与面向对象

1. 概述

早期的编程语言如:C、Basic、Pascal等都是结构化编程语言。结构化程序设计的核心思想就是程序的开发采用自上而下的设计(称为瀑布模式)。对于大型的应用来说,采用函数和库的方式来对代码进行管理。

而C++、 java 、C#、Ruby等都是面向对象的编程语言,将现实世界采用类、对象的概念进行建模。

JAVA进阶 深入理解面向对象

2. 结构化程序设计简介

结构化程序设计方法主张按功能来分析系统需求,其主要原则可概括为自顶向下、逐步求精、模块化等。

结构化程序设计首先采用结构化分析(Structured Analysis,SA)方法对系统进行需求分析,然后使用结构化设计(Structured Design,SD)方法对系统进行概要设计、详细设计、最后采用结构化编程(Structured Program,SP)方法来实现系统。使用这种 SA、SD、SP的方式可以较好的保证软件系统的开发进度和质量。

结构化程序使用三种基本控制结构构造程序,即:

  • 顺序
  • 选择
  • 循环三种基本控制结构构造。

结构化程序设计主要强调的是程序的易读性。

详细描述处理过程常用三种工具:图形、表格和语言。

  • 图形:程序流程图、N-S图、PAD图
  • 表格:判定表
  • 语言:过程设计语言(PDL)

结构化程序设计曾被称为软件发展中的第三个里程碑,要点是:

  • 主张使用顺序、选择、循环三种基本结构来嵌套连结成复杂层次的结构化程序,严格控制GOTO语句的使用
  • 自顶而下、逐步求精
  • 独立功能、单出口、单入口,减少模块的相互联系使模块可作为插件或积木使用,降低程序的复杂性,提高可靠性。

2. 面向对象程序设计

基本思想是使用类、对象、继承、封装、消息等基本概念来进行程序设计。最小的程序单元是类,这些类可以生成系统中的多个对象,而这些对象则直接映像成客观世界的各种事务。采用面向对象方式开发的软件系统逻辑上的组成结构如:

面向对象的软件系统由多个类组成。类还会提供操作这些状态数据的方法,为这些行为提供相应的实现。

二、类的定义与修饰符

1. 定义类

 [修饰符] class 类名
{
    零到多个 构造器 定义..
    零到多个Field…
    零到多个方法…
}  

一个实际的例子:

 public class Person
{
    public String name;
    public int age;
    public void say(String content){
       System.out.println(content);
    }
}
Person p;     //p变量,放在栈内存里
p=new Person();   //Person对象放在堆内存里  

类也是一种引用数据类型,因此程序中定义的Person类型的变量实际上是一个引用,它被存放在栈内存里,指向实际的Person对象,而真正的Person对象则存放在堆(heap)内存中。

Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。不管是数组还是对象,都只能通过引用来访问它们。

堆内存里的对象可以有多个引用,即多个引用变量指向同一个对象。如接上面的程序:

 Person p=p;  

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

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

2. 修饰符

  • public 公共类与公共属性、方法
  • protected 保护类与保护属性、方法
  • private 私有类与私有属性、方法
  • static 静态类与静态属性、方法
  • final 表示修饰的类、方法、变量不可改
  • abstract 虚类
  • abstract和final最多只能出现其中之一,可以与static组合起来修饰方法

3. final修饰符

final类似C#里的sealed关键字,用于表示它修饰的类、方法和变量不可改变

2.3.1 final 成员变量

final修饰的成员变量必须由程序员显示地指定初始值

类 Field:在静态初始化块中或声明该Field时指定初始值

实例Field:必须在非静态初始化块、声明Field或 构造函数 中指定初始值

2.3.2 final 局部变量

2.3.3 final 修饰基本类型变量和引用类型变量的区别

final修饰基本类型变量,变量的值不能改变

但修饰引用类型变量,只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象可以发生改变。

2.3.4 可执行“宏替换”的final变量

对一个final变量来说,不管它是类Field、实例Field,还是局部变量,只要该变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量

  • 使用final修饰符修饰
  • 在定义该final变量时指定了初始值
  • 该初始值可以在编译时就被确定下来
  • 示例:
 public class test{
  public static void main(String[] args)
  {
     final int a=;
     System.out.println(a);
  }
}  

上面所示变量a其实不存在,在编译时就转换成System.out.println(5);

Java使用常量池来管理用过的 字符串 直接量,如String a=“java”;

如果再定义

String b=“java”;

这时a==b 是true。

2.3.5 final方法

final修饰的方法不可被重写。如果不希望子类重写父类的某个方法,就可以使用final修饰该方法 。

Object类的getClass()就是一个final方法。

2.3.6 final 类

final修饰的类不可以有子类。如:java.lang.Math

2.3.7 不可变类(immutable)

创建实例后,该实例的Field是不可改变的。Java的8个包装类和java.lang.String都是不可在。

  • 使用private final修饰Field
  • 提供带参数构造器,用于根据传入参数来初始化类的Field
  • 仅为该类的Field提供getter方法,不要为该类的Field提供setter方法
  • 如果有必要,重写Object类的hashCode和equals方法。

2.3.8 缓存实例的不可变类

Java的Integer类,使用了缓存策略。在使用new构造对象时,返回全新的integer对象。

如果使用valueOf()创建Integer对象,则在创建该对象的同时,会缓存该方法创建的对象

Integer只缓存-128到127之间的Integer对象,超过范围的对象不会缓存。

如果程序经常使用某个不可变类的实例,则可以把该实例保存进缓存,减少系统开销。

三、深入理解java的面向对象

1. 一切皆对象,包装类

在Java语言中,除了8个基本数据类型值,一切都是对象。对于8个基本的数据类型,Java也提供了包装类:

基本数据类型包装类byteByteshortShortintIntegerlongLongcharCharacterfloatFloatdoubleDoublebooleanBoolean

对象是Java程序的核心,所以Java里的对象具有唯一性,每个对象都有一个标识来引用它,如果某个对象失去了标识,这个对象将变成垃圾。Java语言不允许直接访问对象,而是通过对对象的引用来操作对象。

使用示例:

 Float f=new Float("4.56");
Booean blObj = new Boolean(b);  
  • 当包装类创建失败时,会引发
 java.lang.NumberFormatException异常。  
  • 获得包装类的基本类型变量
 int i = integerObject.intValue();  

2. 类成员

Java类包含5种成员:

  • Field
  • 方法
  • 构造器
  • 初始化块
  • 内部类(包括接口、枚举)

类Field(static修饰)属于整个类,当系统第一次准备使用该类时,系统会为该类Field分配内存空间。类Field开始生效,直到该类被卸载,所占内存才会被垃圾回收机制回收。类Field生存范围几乎等同于该类的生存范围。当类的初始化完成后,类Field也被初始化完成。

类Field既可通过类访问,也可通过类的对象来访问。通过类的对象来访问类Field时,实际上访问的不是该类所拥有的Field。而C#其实不允许通过对象访问类Field。

3. 类的结构关系

  • 继承
  • 整体->部分结构关系,也称为组装结构,这是典型的组合关系。Java语言通过在一个类里保存另一个
  • Java语言是纯粹的面向对象的程序设计语言,这主要表现为Java完全支持面向对象的三种基本特征:继承、封装、多态。Java语言完全以对象为中心,Java程序的最小程序单位是类,整个Java程序由一个一个的类组成。
  • Java完全支持使用对象、类、继承、封装、消息等基本概念来进行程序设计,允许从现实世界中客观存在的事物(即对象)出发来构造软件系统,在系统构造中尽可能运行人类的自然思维方式。面向对象的方式实际上由:
  • OOA(面向对象分析)
  • OOD (面向对象设计)
  • OOP(面向对象编程)

三个部分有机组成,其中OOA和OOD的结构需要使用一种方式来描述并记录,目前业界统一采用 UML (统一建模语言)来描述并记录OOA和OOD的结果。

UML的2.0 一共包括13种类型的图形。使用这13种图形中的某些就可以很好地描述并记录软件分析、设计的摘要。通常而言,我们没有必要为软件系统绘制13种UML图形,常用的UML图形有用例图、类图、组件图、部署图、顺序图、活动图和状态图。

4. 面向对象的基本特征

3.4.1 封装(Encapsulation)

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

3.4.2 继承(Inheritance)

面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法

3.4.3 多态(Polymorphism)

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

3.4.4 抽象

忽略一个主题中与当前目标无关的那些方法,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是考虑部分问题。例如,考虑考察Person对象时,我们不可能在程序中把Person的所有细节都定义出来,通常只能定义Person的部分数据、部分行为特征—而这些数据、行为特征是软件系统所关心的部分

3.4.5 其它功能

  • 对象是面向对象方法中最基本的概念,它的基本特点有:标识唯一性、分类性、多态性、封装性、模块独立性好
  • 类是具有相同属性、共同方法的一类事务。类的封装性将各种信息细节隐藏起来,并通过公用方法来暴露该类对外所提供的功能,从而提高了类的内聚性,降低了对象之间的耦合性。
  • 对象间的这种相互合作机制协助进行,这样的机制称为“消息”,消息是一个实例与另一个实例之间相互通信的机制。
  • 在面向对象方法中,类之间共享属性的机制称为继承。继承具有传递性。继承分为单继承(一个继承只允许有一个直接父类,即类等级为树形结构)与多继承(一个类允许有多个直接父类)。Java不支持多继承。

5. 抽象类

3.5.1 抽象类的特性

  • 使用abstract修饰,抽象方法不能有方法体。
  • 抽象类不能被实例化,无法使用new创建实例,只能当作父类被继承
  • 抽象类可以有Field、方法、构造函数、初始化块、内部类、枚举类
  • 含有抽象方法的类只能被定义成抽象类
  • abstract不能用来修饰局部变量、构造函数
  • static 和 abstract不能共同修饰同一个方法

3.5.2 接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行是更彻底,则可以提炼出一种更加特殊的抽象类——接口。接口层不能包含应运,接口里所有方法都是抽象方法。

  1. JAVA接口是一些方法特征的集合,接口中只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被实现并且具有完全不同的行为。当然,JAVA还允许在接口中定义常量。
  2. JAVA接口中的方法只能是abstract和public,接口中不能有构造器,可以有public、static和final属性。
  3. 接口将方法的特征和方法的实现分割开来,这种分割体现在接口常常代表一种角色,它封装与该角色相关的操作和属性,而实现这个接口的类就是扮演这个角色的演员。
  4. 一个接口可以被多个类实现,一个类可以实现多个接口。

3.5.3 接口的定义

 [修饰会] interface 接口中 extends 父接口,父接口2...
{
    常量定义;
    抽象方法定义;
}  

示例:

 package lee;
public interface Output{
   int MAC_CACHE_LINE=;
   void out();
   void getData(String msg);
}  

3.5.4 接口的继承

接口完全支持多继承,一个接口可以有多个直接父接口。多个父接口排在extends后,用,隔开。

3.5.5 使用接口

接口不能用于创建实例,但接口中以用于声明引用类型爆裂。当使用接口为声明引用类型变量地,这个引用类型盘龙乃至其实现类的对象。除此之外,接口的主要用途不是被实现类实现。

示例:

 
interface Product
{
int getProduceTime();
}

public class Printer implements Out , Product{
private String[] printDate=new String[MAX_CACHE_LINE];
private int dataNum=;

public void out(){
while(dataNum>){
System.out.println("打印机打印"+printDate());
System.arraycopy(printDate,,printDate,0,--dataNum);
}
}
public void getData(String msg){
if(dataNum >= MAX_CACHE_LINE){
System.out.println("输出队列已满,添加失败");
}else
{
printData[dataNum++]=msg;
}
}
public int getProducteTime(){
return;
}
public static void main(String[] args){
Output o = new Printer();
o.getData("轻量级Java EE企业应用实战");
o.getData("疯狂Java 讲义");
o.out();
o.getData("疯狂Android讲义");
o.getData("疯狂Ajax讲义");
o.out();
Product p = new Printer();
System.out.println(p.getProduceTime());
//所有接口类型的引用变量都可直接赋给Object类型的变量
Object obj=p;
}
}  

3.5.6 接口和抽象类比较

相同点

  • 接口和抽象类都不能被实例化
  • 接口 抽象类都可以包含抽象方法,继承接口或继承抽象类的普通子类都必须实现这些抽象方法

差别

  • 接口体现的是一种规范。对接口实现者而言,接口规定了实现者必须对外提供哪些服务(以方法的形式为提供);对于接口调用者而言,接口规定了调用者可以调用哪些服务,以及如何去调用这些服务。…

3.5.7 面向接口编程

可以实现简单工厂模式、命令模式等。

6. 内部类

3.6.1 非静态内部类

 public class OuterClass
{

}  

3.6.2 静态内部类

 public class StaticInnerClassTest{
private int prop=5;
private static int prop=9;
static class StaticInnerClass{
private static int age;
public void accessOuterProp(){
System.out.println(prop);
System.out.println(prop);
}
}
}  

3.6.3 局部内部类

定义在代码中间

 public class LocalInnerClass{
public static void main(String[] args){
class InnerBase{
int a;
}
class InnerSub extends InnerBase{
int b;
}
InnerSub is=new InnerSub();
is.a=;
is.b=;
System.out.println(is.a+","+is.b");
}
}  

3.6.4 匿名内部类

 interface Product{
public double getPrice();
public String getName();
}
public class AnonymousTest{
public void test(Product p){
System.out.println("购买了一个"+p.getName() + ",花掉了"+p.getPrice());
public static void main(String[] args){
AnonymousTest ta = new AnonymousTest();
ta.test(new Product(){
public double getPrice(){
return.8;
}
public String getName(){
return "AGP";
}
})
}
}
}  

3.6.5 闭包和回调

闭包(Closure)是一种能被调用的对象,它保存创建它的作用域信息。Java7没有显式支持闭包,但可以把非静态内部类当成面向对象领域的闭包。

通过这种仿闭包的非静态内部类,就可以很方便地实现回调功能。

interface Teachable{
  void work();
}
public class Programmer{
  private String name;
  public Programmer(){}
  public Programmer(String name){
    this.name=name;
  }
  //...getter & setter
  public void work(){
    System.out.println(name + "doing...");
  }
}

public class TeachableProgrammer extends Programmer implements Teachable{
  public void work(){
    System.out.println(getName()+"doing ...");
  }
}  

3.6.6 lambda实现匿名类

java8开始支持lambda表达式,可以看成一种闭包,允许把函数当成参数使用,是面向函数式编程的思想。

lambda表达式语法:

   (paramters) -> expression

 (paramters) -> {statements;}  
    展开如:
    (Type param1, Type2 param2, Type2 param2, ...) -> {
        statement;
        statement;
        statement;
        ...
        return statementX;
    }  

实例:

 //匿名内部类写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("内部类写法");
            }
        }).start();  

改用lambda:

 //lambda 写法
new Thread(() -> System.out.println("lambda写法")).start();      

lambda表达式特征

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

几个示例:

 //入参为空
        TestDemo no_param = () -> "hi, no param";
        TestDemo no_param = () -> { return "hi, no param"; };
        System.out.println(no_param.hi());

        //单个参数
        TestDemo param = name -> name;
        TestDemo param2 = name -> { return name;};
        System.out.println(param.hei("hei, grils"));

        //多个参数
        TestDemo multiple = (String hello, String name) -> hello + " " + name;
        //一条返回语句,可以省略大括号和return
        TestDemo multiple2 = (hello, name) -> hello + name;
        //多条处理语句,需要大括号和return
        TestDemo multiple3 = (hello, name) -> {
            System.out.println("进入内部");
            return hello + name;
        };
        System.out.println(multiple.greet("hello", "lambda"));  

7. 枚举类

 public class Season
{
   private final String name;
   private final String desc;
   public static final Season SPRING=new Season("春天","");
   public static final Season SUMMBER=new Season("夏天","");
   public static final Season FALL=new Season("秋天","");
   ...
   public static Season getSeason(int seasonNum){
   switch(seasonNum){
      case:
         return SPRING;
      case:
         return SUMMBER;
      ...
   }
   }
   private Season(String name,String desc){
      this.name=name;
      this.desc=desc;
   }
   public String getName(){
      return this.name;
   }
   public String getDesc()
   {
      return this.desc();
   }
}  

使用

 public class SeasonTest
{
   public SeasonTest(Season s)
   {
      System.out.println(s.getName());
   }
   public static void main(String[] args)
   {
      new SeasonTest(Season.FALL);
   }
}  

3.7.1 使用enum 枚举类

SeasonEnum.java

 public enum SeasonEnum
{
   SPRING,SUMMBER,FALL,WINTER;
}  

EnumTest.java

 public class EnumTest
{
   public void judge(SeasonEnum s)
   {
       switch(s)
       {
             case SPRING:
                System.out.println("描述");
                break;
             ...
       }

   }
   public static void main(String[] args)   
   {
      for(SeasonEnum s:SeasonEnum.values()){
         System.out.println(s);
      }
      new EnumTest().judge(SeasonEnum.SPRING);
   }
}  

java.lang.Enum类提供了以下方法:

  • int compareTo(E o)
  • String name()
  • int ordinal()
  • String toString()
  • public static <T extends Enum> T valueOf(Class enumType,String name)

3.7.2 枚举类的Field,方法 和构造函数

枚举类也可以定义Field、方法。

枚举为的实例只能是枚举值,不能随意通过new创建枚举类对象。

在setName里可以对赋值进行验证。

3.7.3 实现接口的枚举类

枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。

8. 对象的this引用

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

  • 构造器中引用该构造器正在初始化的对象
  • 在方法中引用调用该方法的对象
  • this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或Field。但要注意的是,在static修饰的方法里不能使用this引用,且静态成员不能直接访问非静态成员。

有些时候this可以省略,编译器会自动在引用内部方法时加上this.classname.方法名。

Java允许使用对象来调用static修饰的Field方法,但应避免这样使用,现代的IDE一般会给出报警。

9. 对象的方法

3.9.1 参数值的传递方式

Java的方法参数传递只有一种方式:值传递,这是因为Java没有指针的概念。正如前面提到过的,Java的一个对象可以拆为两部分:

  • 指向对象的变量名,存在栈中
  • 对象真正内容,存在堆中

当把一个对象作为参数时,传递的值是对象的引用地址,即栈里的变量复制了,但指向的堆仍然没有变。

为了便于理解,往往可以认为Java传递复杂参数时(非8种基本类型)采用的是按引用传值,但要注意面试时不能这样讲。

3.9.2 形参个数可变的方法

 public class Varargs{
   public static void test(int a,String... books)
   {
       for(String tmp : books)
       {
          System.out.println(tmp);
       }
   }
   public static void main(String[] args)
   {
        test(,"参数2","参数3");
   }
}  

11. 继承

3.11.1 super限定

如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法:

 public void callOverrideMethod()
{
   super.fly();
}  

3.11.2 多态

Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。如果编译与运行时类型不一到处,就可能出现所谓的多态(Polymorphism)

3.11.3 instanceof运算符

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的实例,或者其子类,实现类的实例。

3.11.4 继承与组合

初始化块

初始化块是Java类里可出现的第4种成员,一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化后执行

 public class Person
{ 
   {
        ; //先执行
   }
   {
        ; //
   }
   public Person()
   {
       ; //
   }
   public static void main(String[] args)
   {
      new Person();
   }
}  

初始化块只在创建Java对象时隐匿执行,而且在构造函数之前执行。

12. 静态初始化块

在类初始化阶段执行静态初始化块,因此比普通初始化块先执行。

静态初始化块会一直上溯到顶父类静态初始化类先执行,最后才执行该类的静态初始化块。

JDK1.5后新增引入静态成员:

 import static java.lang.Math.*;  

可以引入所有静态Field和方法。

13. 打印对象和toString方法

 class Person{}
Person p=new Person();

//下面两句效果是一样的
System.out.println(p);
System.out.println(p.toString());  
  • 所有Java对象都有toString实例方法
  • Java对象都可以与字符串进行连接运算
  • Object类提供的toString方法总是返回该对象实现类的“类名+@+hashCode”
  • toString()可以重写

14. == 和 equals

Java程序中测试两个变量是否相等有两种方式。

  • ==判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则只要两个变量的值相等,即返回true
  • 对于两个引用类型的变量,必须指向同一个对象时,==才返回true
  • ==不可用于比较类型上没有父子关系的两个对象

15. “hello” 和 new String(“hello”)

  • “hello”,JVM将会使用常量池来管理这些字符串
  • 当使用new String(“hello”)时,JVM会先使用常量池来管理”hello”直接量,再用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。new String(“hello”)一共产生两个对象。

常量池(constant pool)专门用于管理在编译期被确定并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量。

JVM常量池保证相同的字符串(在编译期间可运算的值)直接量只有一个,不会产生多个副本。

使用new String()创建的字符串对象是运行时创建出来的,它被保存在运行时内存区内(堆内存),不会放入常量池中。

要判断两个引用变量的值是否相等时,可以使用.equals判断。equals()方法也可以重写。

16. 自动装箱与自动拆箱(Autoboxing AutoUnboxing)

从JDK1.5之后,提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。即:

 Integer intObj=;
boolean b = (Boolean)boolObj;  
  • 包装类的parseXxx(String)静态方法提供了字符串值转换为基本类型的值的方法;也可以使用包装类的构造器实现
  • String 类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串
  • 包装类的实例可与数值类型的值进行比较
  • 而两个包装类的实例进行比较的情况比较复杂,只有两个包装类引用指向同一个对象时才会返回true
  • 自动装箱的特例
 Integer a=;
Integer b=;
a==b  //true

Integer a=;
Integer b=;
a==b  //false  

其原因是-128~127之间的整数自动装箱成一个Integer实例时,会放入一个名为cache的数组中缓存起来。不在这个范围内的,系统总是重新创建一个Integer实例。

  • Java7 增强了包装类的功能,为所有包装类提供了一个静态的compare(xxx var1,xxx val2)方法,以方便进行值比较

四、类的设计原则

一、类的设计原则

1.单一职责原则(SRP)

每个类只担任一个职责,每个类应只有一个引起它变化的原因。

一个类只负责一项职责,应该仅有一个引起它变化的原则

2.里氏替换原则(LSP)和依赖倒置原则(DIP)

子类可以扩展父类的功能,但不能改变父类原有的功能

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

3.依赖倒置原则(DIP)

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。即针对接口编程,不要针对实现编程

依赖倒置原则的中心思想是面向接口编程,传递依赖关系有三种方式,以上的说的是是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递。

  1. 低层模块尽量都要有抽象类或接口,或者两者都有。
  2. 变量的声明类型尽量是抽象类或接口。
  3. 使用继承时遵循里氏替换原则。

4.接口隔离原则(ISP)

建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

5.开闭原则(OCP)

开闭原则要求软件有一个良好的基本结构,确保面对“变化”的时候,仅仅扩展而不是修改现有对象的组织框架就可以随需而动。

抽象化是关键,定义抽象类或接口,使其可以有很多的具体实现预见所有的需要,在任何情况下都不再修改上层商业逻辑。

抽象类的用法:

  1. 抽象类应当拥有尽可能多的共同代码
  2. 在一个抽象类到多个具体类的继承关系中,共同的代码应当尽量移动到抽象类中,这样可以提高代码利用率。当需要修改这些共同的代码时,设计师只需要修改一个地方。
  3. 抽象类应当拥有尽可能少的数据
  4. 与代码移动方向相反,数据的移动方向是从抽象类到具体类,一个数据对象无论是否使用都会占用资源,因此数据应当尽量放到具体类或等级结构的低端。

五、对象与垃圾回收

垃圾回收只负责回收堆内存中的对象,不会回收任何物理资源

程序无法精确控制垃圾回收的运行

垃圾回收前,会调用 finalize ()方法,该方法可能使该对象重新复活,从而导致垃圾回收机制取消回收

1. 对象在内存中的状态

  • 可达状态:一个对象被创建后,若有一个以上的引用变量引用它,就处理可达状态
  • 可恢复状态:不再有变量引用,即进入了可恢复状态
  • 不可达状态:已经调用finalize()后依然没有使该对象变成可达状态

2. 强制垃圾回收

  • 调用System.gc()静态方法
  • 调用Runtime.getRuntime().gc()实例方法

测试:

 
public class GcTest {
public static void main(String[] args){
for(int i=;i<4;i++){
new GcTest();
}
}
public void finalize()
{
System.out.println("系统正在清理资源");
}
} 

可以看到finalize()不会被调用。

而如果:

 
public class GcTest {
public static void main(String[] args){
for(int i=;i<4;i++){
new GcTest();
Runtime.getRuntime().gc();
}
}
public void finalize()
{
System.out.println("系统正在清理资源");
}
} 

则finalize()会被调用。

使用java -verbose:gc GcTest查看垃圾回收后的提示信息:

JAVA进阶 深入理解面向对象


但上面语句只是建议垃圾回收,系统仍有可能不立即进行垃圾回收。

3. finalize方法

  • 不要调动调用对象的finalize()方法
  • finalize()方法调用时间有不确定性,不要把它当成一定会被执行的方法
  • 当JVM执行可恢复对象的finalize()方法时,可能使对象或系统中其它对象重新变成可达状态
  • 当JVM执行finalize()方法出现异常时,垃圾回收机制不会报告异常,程序继续执行。

System和Runtime类提供了runFinalization方法,可以强制垃圾回收机制调用系统中可恢复对象的finalize()方法

4. 对象的软、弱、虚引用


六、使用JAR文件

常用命令:

1. 创建JAR文件

 jar cf test.jar test   //test是目录  

2. 创建JAR文件并显示压缩过程

 jar cvf test.jar test  

3. 不使用清单文件

 jar cvfM test.jar test  

4. 自定义清单文件内容

 jar cvfm test.jar manifest.mf test  

5. 查看jar包内容

 jar tf test.jar  

6. 查看JAR包详细内容

 jar tvf test.jar  

7. 解压缩

 jar xf test.jar  

8. 带提示信息解压缩

 jar xvf test.jar  

9. 更新JAR文件

 jar uf test.jar Hello.class  

10. 更新时显示详细信息

 jar uvf test.jar Hello.class  

11. 创建可执行的jar包

程序发布的方式:

  • 使用平台相关的编译器,使用第三方编译器
  • 为应用编辑一个批处理,如windows
 java package.MainClass  

 start javaw package.MainClass  
  • 将一个应用程序制作成可执行的JAR包,通过JAR包来发布应用程序
 jar cvfe test.jar Test *.class  

该命令把目录下的*.class压缩到test.jar,并指定使用Test类为程序入口。

运行的方法:

 java -jar test.jar
javaw test.jar  

七、UML(统一建模语言)介绍

面向对象软件开发需要经过OOA(面向对象分析)、OOD(面向对象设计)和OOP(面向对象编程)三个阶段,OOA对目标系统进行分析,建立分析模型,并将之文档化;OOD用面向对象的思想对OOA的结果进行细化,得出设计模型。OOA和OOD的分析、设计结果需要统一的符号来描述、交流并记录,UML就是这种用于描述、记录OOA和OOD结果的符号表示法。

面向对象的分析与设计方法在20世纪80年代末至90年代出现了一个高潮,UML是这个高潮的产物。在些期间出现了三种具有代表性的表示方法。Booch是面向对象方法最早的倡导者之一,他提出了面向对象软件工程的概念。Booch1993表示法比较适合于系统的设计和构造。

Rumbaugh等人提出了面向对象的建模技术(OMT)方法,采用面向对象的概念,并引入了各种独立于语言的表示符。这种方法用对象模型、动态模型、功能模型和用例模型共同完成对整个系统的建模,所定义的概念和符号可用于软件开发的分析、设计和实现的全过程,软件开发人员不必在开发过程的不同阶段进行概念和符号的转换。OMT-2特别适用于分析和描述以数据为中心的信息系统。

Jacobson于1994年提出了OOSE方法,其最大特点是面向用例(Use-Case),并在用例的描述中引入了外部角色的概念。用例的概念是精确描述需求的重要武器,但用例贯穿于整个开发过程,包括对系统的测试和验证。OOSE比较适合支持商业工程和需求分析。

UML统一了Booch、Rumbaugh和Jacobson的表示方法,而且对其进行了进一步的发展,并最终统一为大众所接受的标准建模语言。UML是一种定义良好、易于表达、功能强大且普遍适用的建模语言,它的作用域不限于支持面向对象的分析与设计,还支持从需求分析开始的软件开发全过程。

UML的大致发展过程如图所示:

JAVA进阶 深入理解面向对象


UML2.0一共包括13种正式图形:

  • 活动图 activity diagram
  • 类图 class diagram
  • 通信图 communication diagram
  • 组件图 component diagram
  • 复合结构图 composite structure diagram
  • 部署图 deplyment diagram
  • 交互概观图 interactive overview diagram
  • 对象图 object diagram
  • 包图 package diagram
  • 顺序图 sequence diagram
  • 状态机 图 state machine diagram
  • 定时图 timing diagram
  • 用例图 use case diagram
  • 很少有一个软件系统在分析、设计阶段对每个细节都使用13种图形来表现。

1. 用例图

用例图用于描述系统提供的系列功能,而每个用例则代表系统的一个功能模块。用例图的主要目的是帮助开发团队以一种可视化的方式理解系统的需求功能,用例图对系统的实现不作任何说明,仅仅是系统功能的描述。

用例图包括用例(以一个椭圆表示,用例的名称放在椭圆的中心或椭圆下面)、角色(Actor,也就是与系统交互的其它实体,以一个人形符号表示)、角色和用例之间的关系(以简单的线段来表示),以及系统内用例之间的关系。用例图一般表示出用例的组织关系—要么是整个系统的全部用例,要么是完成具体的一组用例。下面是BBS系统的部分用例示意图。


JAVA进阶 深入理解面向对象


用例图通常用于表达系统或者系统范畴的高级功能。

用例图主要在需求分析阶段使用,用于描述系统实现的功能,方便与客户交流,保证系统需求的无二性,用实例图表示系统外观,不要指望用例图和系统的各个类之间有任何联系。不要把用例做得过多,过多的用例将导致难以阅读,难以理解;尽可能多地使用文字说明。

2. 类图

类图是最古老、功能最丰富、使用最广泛的UML图。类图表示系统中应该包含哪些实体,各实体之间如何关联;换句话说,它显示了系统的静态结构,类图可用于表示逻辑类,逻辑类通常就是业务人员所谈及的事物种类。

类在类图上使用包含三个部分的矩形来描述,最上面的部分显示类的名称,中间部分包含类的属性,最下面的部分包含类的方法。

类之间有三种基本关系:

  • 关联(包括聚合、组合)
  • 泛化(与继承同一个概念)
  • 依赖

关联

关联具有一定的方向性,如果仅能从一个类单方向地访问另一个类,则被称为单向关联;如果两个类可以互相访问对象,则称为双向关联。一个对象能访问关联对象的数目被称为多重性。关联使用一条实线表示,带箭头的实线表示单向关联。

关联关系包括两种特例:聚合和组合。它们都有部分和整体的关系,但通常认为组合比聚合更加严格。如:

  • 学生既可以是篮球俱乐部的成员,也可以是书法俱乐部的成员,称为聚合。使用空心菱形框的实线表示
  • 当某个实体组合成另一个实体时,该实体则不能同时是一个实体的部分,使用实心菱形框表示:

JAVA进阶 深入理解面向对象


图中描述Teacher与Student之间的关联关系:

  • 它们是双向关联关系,而且使用了多重性来表示Teacher和Student之间存在1:N的关联关系(1…*表示可以是一个到多个),即一个Teacher实体可以有1个或多个关联的Student实体;Student和BasketBallClud存在聚合关系,即1个或多个Student实体可以聚合成一个BascketBallClud实体;而Arm和Student之间存在组合关系。2个Arm实体组合成一个Student实体。

泛化

泛化与继承是同一个概念,都是指子类是一种特殊的父类,类与类之间的继承关系是非常普遍的,继承关系使用带空心三角形的实线表示。

从下图可以看出,Student和Person类之间的继承关系。

JAVA进阶 深入理解面向对象


依赖

如果一个类的改动会导致另一个类的改动,则称两个类之间存在依赖。依赖关系使用带箭头的虚线表示,其中箭头指向被依赖的实体。依赖的常见可能原因如下:

  • 改动的类将消息发给另一个类
  • 改动的类以另一个类作为数据部分
  • 改动的类以另一个类作为操作参数
  • 通常而言,依赖是单向的,尤其是当数据表现和数据模型分开设计时,数据表现依赖于数据模型。例如:JDK基础类库中的JTable和DefaultTableModel。

JAVA进阶 深入理解面向对象


3. 组件图

对于现代的大型应用程序而言,通常不只是单独一个类或单独一组类所能完成的,通常会由一个或多个可部署组件组成。对Java程序而言,可复用的组件通常打包成一个JAR、WAR等文件;对C/C++应用而言,通常是DLL动态链接库文件。

组件图提供系统的物理视图,它的用途是显示系统中的软件对其它软件组件的依赖关系。组件图可以在一个非常高的层次上显示,仅显示系统中粗粒度的组件,也可以在组件包层次上显示。

组件图通常包含组件、接口和Port等图元,UML使用带插头符号的矩形来表示组件,使用圆圈代表接口,使用位于组件边界上的小矩形代表Port。

组件的接口表示它能对外提供的服务规范,这个接口通常有两种表现形式:

  • 用一条实线连接到组件逻辑的圆圈表示
  • 使用位于组件内部的圆圈表示
  • 组件除了可以对外提供服务接口之外,还可能依赖于某个接口,组件依赖于某个接口使用一条带半圆的实现来表示。如图一个简单的Order组件,对外提供一个Payable接口,该组件需要依赖于一个CustomerLookup接口——通常这个CustomerLookup接口也是系统中已有的接口。

JAVA进阶 深入理解面向对象

4. 部署图

部署图用于描述软件系统如何部署到硬件环境中,它的用途是显示软件系统不同的组件将在何处物理运行,以及它们将如何彼此通信。


JAVA进阶 深入理解面向对象


5. 顺序图

顺序图显示具体用例(或者是用例的一部分)的详细流程,并且显示流程中不同对象之间的调用关系,同时还可以很详细地显示对不同对象的不同调用。顺序图描述了对象之间的交互(顺序图和通信图都被称为交互图),重点在于描述消息及其时间顺序。

顺序图有两个维度:垂直维度,以发生的时间顺序显示消息/调用的序列;水平维度,显示消息被发送的对象实例。

顺序图的顶部每个框表示每个类的实例(对象),框中的类实例名称和类名称之间用冒号或空格来分隔,例如myReportGenerator:ReportGenerator。如果某个类实例向另一个类实例发送一条消息,则绘制一条指向接收类实例的带箭头的连线,并把消息/方法的名称放在连线上面。

对于某些特别重要的消息,还可以绘制一条带箭头的指向发起类实例的虚线,将返回值标注在虚线上,绘制带返回值的信息可以使得序列图更易于阅读。

用户登录顺序图:

JAVA进阶 深入理解面向对象


当绘制顺序图时,消息可以向两个方向扩展,消息穿梭在顺序图中,通常应该把消息发送者与接收者相邻摆放,尽量避免消息跨越多个对象。对象的激活期不是其存在的时间,而是它占据CPU的执行时间,绘制顺序图时,激活期要精确。

6. 活动图

活动图和状态机图都被称为演化图。

JAVA进阶 深入理解面向对象


7. 状态机图

JAVA进阶 深入理解面向对象

上图描绘了Hibernate 实体具有3个状态:瞬态、持久化和脱管。

绘制状态机图时应该保证对象只有一个初始状态,可以有多个终结状态。状态要表示对象的关键快照,有重要的实际意义,无关紧要的状态则无须考虑,绘制状态机时事件和方法要明确。

状态机图擅长表现单个对象的跨用例行为,对于多个对象的交互行为应该考虑采用顺序图,不要对系统的每个对象都画状态机图,只对真正需要关心各个状态的对象才绘制状态机图。

本文学习资源来自《疯狂Java讲义》。UML部分摘自一本书忘了名称,如有侵权请告知,我将立刻予以删除。