Java基础

时间:2020-05-28
本文章向大家介绍Java基础,主要包括Java基础使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Java基础

1. Java关键字

1.1 基本类型
1.1.1 boolean
  • 大小:—(boolean类型没有给出具体的占用字节数,因为对虚拟机来说根本就不存在 boolean 这个类型,boolean类型在编译后会使用其他数据类型来表示。)

    《Java虚拟机规范》一书中的描述:“虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个boolean元素占8位”。可以得出boolean类型单独使用是4个字节,在数组中使用是1个字节。

  • 可表示范围:true或false

  • 默认值:false

1.1.2 byte
  • 大小:8位

  • 可表示范围:-128~127

  • 默认值:0

1.1.3 char
  • 大小:16位

  • 可表示范围:0~255

  • 默认值:‘\u0000‘

1.1.4 double
  • 大小:64位

  • 可表示范围:-1.7E308~1.7E308

  • 默认值:0.0

1.1.5 float
  • 大小:32位

  • 可表示范围:-3.4E38~3.4E38

  • 默认值:0.0

1.1.6 int
  • 大小:32位

  • 可表示范围:-2147483648~2147483647

  • 默认值:0

1.1.7 long
  • 大小:64位

  • 可表示范围:-9223372036854775808~9223372036854775807

  • 默认值:0

1.1.8 short
  • 大小:16位

  • 可表示范围:-32768-32767

  • 默认值:0

1.2 程序控制语句
1.2.1 do......while
  • 对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。

  • 用法:

do {
      //代码语句
}while(布尔表达式);

注:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了。 如果布尔表达式的值为 true,则语句块一直执行,直到布尔表达式的值为 false。

  • 示例:

public static void main(String[] args) {
   int x = 10;
   do{
       System.out.println("value of x : " + x );
       x++;
  }while( x < 20 );
}

输出:

value of x : 10
value of x : 11
value of x : 12
value of x : 13
value of x : 14
value of x : 15
value of x : 16
value of x : 17
value of x : 18
value of x : 19
1.2.2 while
  • 最基本打循环方式

  • 用法:

    while( 布尔表达式 ) {  //循环内容 }

    直到布尔表达式为false才会停止。

  • 示例:

public static void main(String[] args) {
   int x = 10;
   while( x < 20 ){
       System.out.println("value of x : " + x );
       x++;
  }
}

输出:

value of x : 10
value of x : 11
value of x : 12
value of x : 13
value of x : 14
value of x : 15
value of x : 16
value of x : 17
value of x : 18
value of x : 19
1.2.3 if......else

一个 if 语句包含一个布尔表达式和一条或多条语句。

if 语句后面可以跟 else 语句,当 if 语句的布尔表达式值为 false 时,else 语句块会被执行。

  • 用法:

if(布尔表达式) {   //如果布尔表达式为true将执行的语句 }
if(布尔表达式){   //如果布尔表达式的值为true }
   else{   //如果布尔表达式的值为false }
  • 示例:

public static void main(String[] args) {
   int x = 30;

   if( x < 20 ){
       System.out.println("这是 if 语句");
  }else{
       System.out.println("这是 else 语句");
  }
}

输出:

这是 else 语句
1.2.4 for
  • while循环在执行之前是不知道次数的,for循环在循环次数执行之前就已经确定了。

  • 用法:

for(初始化; 布尔表达式; 更新) {    //代码语句 }

说明:

  • 最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。

  • 然后,检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。

  • 执行一次循环后,更新循环控制变量。

  • 再次检测布尔表达式。循环执行上面的过程。

  • 示例:

public static void main(String[] args) {
   for(int x = 10; x < 20; x = x+1) {
       System.out.println("value of x : " + x );
  }
}

输出:

value of x : 10
value of x : 11
value of x : 12
value of x : 13
value of x : 14
value of x : 15
value of x : 16
value of x : 17
value of x : 18
value of x : 19
  • foreach

    • 用法

      for(声明语句 : 表达式) {   //代码句子 }
    • 示例:

      public static void main(String[] args) {
         int [] numbers = {10, 20, 30, 40, 50};

         for(int x : numbers ){
             System.out.print( x );
             System.out.print(",");
        }
         System.out.print("\n");
         String [] names ={"James", "Larry", "Tom", "Lacy"};
         for( String name : names ) {
             System.out.print( name );
             System.out.print(",");
        }
         System.out.print("\n");
      }

      输出:

      10,20,30,40,50,
      James,Larry,Tom,Lacy,
1.2.5 switch case default break
  • break 关键字

break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。

break 跳出最里层的循环,并且继续执行该循环下面的语句。

  • switch case default

    switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。

    switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。

    switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。

    case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。

    当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。

    当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。

    switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。

    switch case 执行时,一定会先进行匹配,匹配成功返回当前 case 的值,再根据是否有 break,判断是否继续输出,或是跳出判断。

    • 用法

    switch(expression){    
    case value :       //语句       
    break; //可选    
    case value :       //语句       
    break; //可选    
    //你可以有任意数量的case语句    
    default : //可选       
    //语句 }
    
    • 示例:

    public static void main(String[] args) {
        char grade = 'C';
        switch (grade) {
            case 'A':
                System.out.println("优秀");
                break;
            case 'B':
            case 'C':
                System.out.println("良好");
                break;
            case 'D':
                System.out.println("及格");
                break;
            case 'F':
                System.out.println("你需要再努力努力");
                break;
            default:
                System.out.println("未知等级");
        }
        System.out.println("你的等级是 " + grade);
    }
    
    • 输出:

    良好
    你的等级是 C
    
1.2.6 continue return
  • continue

continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。

在 for 循环中,continue 语句使程序立即跳转到更新语句。

在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。

示例:

public static void main(String[] args) {
    int [] numbers = {10, 20, 30, 40, 50};

    for(int x : numbers ) {
        if( x == 30 ) {
            continue;
        }
        System.out.println( x );
    }
}

输出:

10
20
40
50
  • return

return:必须放在方法中 return的主要作用有两点:

  1. 返回方法指定类型值

  2. 用于方法结束的标志,return 后面的语句不会被执行

示例:

public static void main(String[] args) {
    int i;
    System.out.println("return语句之前"+getInfo());
    for (i = 0; i < 5; i++) {
        if(i==3){
            return;//无返回类型,用于方法的结束
        }
        System.out.println(String.format("i=%d",i));
    }
    //return 之后的语句将不会被执行
    System.out.println("return语句之后"+getInfo());
}
public static int getInfo(){
    return 1;//有返回类型,返回方法指定类型的返回值
}

输出:

return语句之前1
i=0
i=1
i=2
1.2.7 instanceof
  • instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例。

  • 用法:

boolean result = obj instanceof Class

其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。

  注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。

Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true 
1.3 包相关
1.3.1 import

为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 "import" 语句可完成此功能。

在 java 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为:

import android.util.Log;
1.3.2 package

为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。

1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。 2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。 3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。

示例:

package com.example.myapplication;
1.4 访问控制
1.4.1 private

私有访问修饰符是最严格的访问级别,所以被声明为 private 的方法、变量和构造方法只能被所属类访问,并且类和接口不能声明为 private

声明为私有访问类型的变量只能通过类中公共的 getter 方法被外部类访问。

Private 访问修饰符的使用主要用来隐藏类的实现细节和保护类的数据。

public class Logger {   
	private String format;   
	public String getFormat() {      
		return this.format;   
	}   
	public void setFormat(String format) {      
		this.format = format;   
	} 
}

Logger 类中的 format 变量为私有变量,所以其他类不能直接得到和设置该变量的值。为了使其他类能够操作该变量,定义了两个 public 方法:getFormat() (返回 format的值)和 setFormat(String)(设置 format 的值)

1.4.2 protected

protected 需要从以下两个点来说明:

  • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;

  • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。

protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)

1.4.3 public

被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。

如果几个相互访问的 public 类分布在不同的包中,则需要导入相应 public 类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。

public static void main(String[] arguments) {   // ... }
1.5 变量引用
1.5.1 super

super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

  • super的三种使用方法:

    • 直接引用

    与 this 类似,super 相当于是指向当前对象的父类,这样就可以用 super.xxx 来引用父类的成员。

    • 子类中的成员变量或方法与父类中的成员变量或方法同名

    public class Type extends Country{
        String name;
        void value() {
            name = "Shanghai";
            super.value();      //调用父类的方法
            System.out.println(name);
            System.out.println(super.name);
        }
    
        public static void main(String[] args) {
            Type c=new Type();
            c.value();
        }
    }
    
    class Country {
        String name;
        void value() {
            name = "China";
        }
    }
    

    输出:

    Shanghai
    China
    
    • 引用构造函数

      super:调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。

      this:调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

      public class Chinese extends Person{
          Chinese() {
              super(); // 调用父类构造方法(1)
              prt("子类·调用父类无参数构造方法:" +"A chinese coder.");
          }
      
          Chinese(String name) {
              super(name);// 调用父类具有相同形参的构造方法(2)
              prt("子类·调用父类含一个参数的构造方法:" +"his name is " + name);
          }
      
          Chinese(String name, int age) {
              this(name);// 调用具有相同形参的构造方法(3)
              prt("子类:调用子类具有相同形参的构造方法:his age is " + age);
          }
      
          public static void main(String[] args) {
              Chinese cn = new Chinese();
              cn = new Chinese("codersai");
              cn = new Chinese("codersai", 18);
          }
      }
      class Person {
          public static void prt(String s) {
              System.out.println(s);
          }
      
          Person() {
              prt("父类·无参数构造方法: "+"A Person.");
          }//构造方法(1)
      
          Person(String name) {
              prt("父类·含一个参数的构造方法: "+"A person's name is " + name);
          }//构造方法(2)
      }
      

    输出:

    父类·无参数构造方法: A Person.
    子类·调用父类无参数构造方法:A chinese coder.
    父类·含一个参数的构造方法: A person's name is codersai
    子类·调用父类含一个参数的构造方法:his name is codersai
    父类·含一个参数的构造方法: A person's name is codersai
    子类·调用父类含一个参数的构造方法:his name is codersai
    子类:调用子类具有相同形参的构造方法:his age is 18
    
1.5.2 this

this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针

this 的用法在 Java 中大体可以分为3种:

  • 1.普通的直接引用

这种就不用讲了,this 相当于是指向当前对象本身。

  • 2.形参与成员名字重名,用 this 来区分:

class Person {    
	private int age = 10;    
	public Person(){    
		System.out.println("初始化年龄:"+age); 
	}    
	public int GetAge(int age){        
		this.age = age;        
		return this.age;    
	} 
}  
public class test1 {    
	public static void main(String[] args) {       
		Person Harry = new Person();       
		System.out.println("Harry's age is "+Harry.GetAge(12));    
	} 
}

输出:

初始化年龄:10
Harry's age is 12
this和super异同

super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句) this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句) super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) this:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名) 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。 super() 和 this() 类似,区别是,super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。 super() 和 this() 均需放在构造方法内第一行。 尽管可以用this调用一个构造器,但却不能调用两个。 this 和 super 不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。

1.5.3 void

有一类函数,调用后并不需要向调用者返回函数值, 这种函数可以定义为“空类型”。其类型说明符为void。

Void - java.lang 中的 类

Void 类是一个不可实例化的占位符类,它保持一个对代表 Java 关键字 void 的 Class 对象的引用。

示例:

public static void prt(String s) {
    System.out.println(s);
}
1.6 类、方法和变量修饰符
1.6.1 class interface abstract
  • class

一个类可以包含以下类型变量:

局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。

成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。

类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。

一个类可以拥有多个方法,在下面的例子中:barking()、hungry()和sleeping()都是Dog类的方法。

示例:

public class Dog{  
	String breed;  
	int age;  
	String color;  
	void barking(){  }   
	void hungry(){  }   
	void sleeping(){  } 
}
  • interface

有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。

接口(interface)是抽象方法和常量值的定义的集合。

定义一个Runner 接口:

public interface Runner {
    int id = 1;
    int age = 0;
    String name = "nice";
    public void start();
    public void run();
    public void stop();
}

实现Runner 接口:

public class People implements Runner {
    @Override
    public void start() {
        System.out.println("Start!");
    }

    @Override
    public void run() {
        System.out.println("run!");
    }

    @Override
    public void stop() {
        System.out.println("stop!");
    }

    public static void main(String[] args) {
        System.out.println("This is a people!");
        People p = new People();
        p.start();
        p.run();
        p.stop();
    }
}

输出:

This is a people!
Start!
run!
stop!

注意:

重写接口中声明的方法时,需要注意以下规则: 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则: 一个类可以同时实现多个接口。 一个类只能继承一个类,但是能实现多个接口。 一个接口能继承另一个接口,这和类之间的继承比较相似。

  • 接口与类相似点:

    • 一个接口可以有多个方法。

    • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。

    • 接口的字节码文件保存在 .class 结尾的文件中。

    • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

  • 接口与类的区别:

    • 接口不能用于实例化对象。

    • 接口没有构造方法。

    • 接口中所有的方法必须是抽象方法。

    • 接口不能包含成员变量,除了 static 和 final 变量。

    • 接口不是被类继承了,而是要被类实现。

    • 接口支持多继承。

  • 接口特性

    • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。

    • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。

    • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

  • abstract

abstract表示抽象的意思,在java中通常用来修饰抽象类和抽象方法。

    • 抽象类

      如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

      抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。 父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。 在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

      示例:

      创建抽象类:

      public abstract class Employee
      {
         private String name;
         private String address;
         private int number;
         public Employee(String name, String address, int number)
         {
            System.out.println("Constructing an Employee");
            this.name = name;
            this.address = address;
            this.number = number;
         }
         public double computePay()
         {
           System.out.println("Inside Employee computePay");
           return 0.0;
         }
         public void mailCheck()
         {
            System.out.println("Mailing a check to " + this.name
             + " " + this.address);
         }
         public String toString()
         {
            return name + " " + address + " " + number;
         }
         public String getName()
         {
            return name;
         }
         public String getAddress()
         {
            return address;
         }
         public void setAddress(String newAddress)
         {
            address = newAddress;
         }
         public int getNumber()
         {
           return number;
         }
      }
      

      继承抽象类:

      public class Salary extends Employee
      {
         private double salary; //Annual salary
         public Salary(String name, String address, int number, double
            salary)
         {
             super(name, address, number);
             setSalary(salary);
         }
         public void mailCheck()
         {
             System.out.println("Within mailCheck of Salary class ");
             System.out.println("Mailing check to " + getName()
             + " with salary " + salary);
         }
         public double getSalary()
         {
             return salary;
         }
         public void setSalary(double newSalary)
         {
             if(newSalary >= 0.0)
             {
                salary = newSalary;
             }
         }
         public double computePay()
         {
            System.out.println("Computing salary pay for " + getName());
            return salary/52;
         }
      }
      

      调用:

        public static void main(String [] args)
         {
            Salary s = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 3600.00);
            Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);
       	  System.out.println("Call mailCheck using Salary reference --");
        	  s.mailCheck();
       
       	  System.out.println("\n Call mailCheck using Employee reference--");
        	  e.mailCheck();
      }
      

      输出:

      Constructing an Employee
      Constructing an Employee
      Call mailCheck using  Salary reference --
      Within mailCheck of Salary class
      Mailing check to Mohd Mohtashim with salary 3600.0
      
      Call mailCheck using Employee reference--
      Within mailCheck of Salary class
      Mailing check to John Adams with salary 2400.
      
    • 抽象方法

      Abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。

      抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。

      示例:

      public abstract class Employee
      {
         private String name;
         private String address;
         private int number;
      
         public abstract double computePay();
      
         //其余代码
      }
      

      关于抽象方法:

      • 如果一个类包含抽象方法,那么该类必须是抽象类。

      • 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

      继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

      示例:

public class Salary extends Employee
{
   private double salary; // Annual salary

   public double computePay()
   {
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }

   //其余代码
}

抽象类和抽象方法总结:

  • 抽象类不能被实例化,如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。

  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。

  • 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。

  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

1.6.2 extends implements
  • extends

继承父类,使用格式:

class B extends A
  • implements

实现接口,使用格式:

class A  implements C,D,E

extends和implements总结:

extends 是继承父类,只要那个类不是声明为final或者那个类定义为abstract的就能继承,JAVA中不支持多重继承,但是可以用接口来实现,这样就用到了implements,继承只能继承一个类,但implements可以实现多个接口,用逗号分开就行了。

1.6.3 final

可以用于修饰类,成员变量,成员方法。

特点

  1. 它修饰的类不能被继承。

  2. 它修饰的成员变量是一个常量。

  3. 它修饰的成员方法是不能被子类重写的。

final修饰成员变量,必须初始化,初始化有两种

  • 显示初始化;

  • 构造方法初始化。 但是不能两个一起初始化

final和private的区别

  1. final修饰的类可以访问; private不可以修饰外部类,但可以修饰内部类(其实把外部类私有化是没有意义的)。

  2. final修饰的方法不可以被子类重写; private修饰的方法表面上看是可以被子类重写的,其实不可以,子类是看不到父类的私有方法的。

  3. final修饰的变量只能在显示初始化或者构造函数初始化的时候赋值一次,以后不允许更改; private修饰的变量,也不允许直接被子类或一个包中的其它类访问或修改,但是他可以通过set和get方法对其改值和取值。

1.6.4 static

在类中,用static声明的成员变量为静态成员变量,也成为类变量。类变量的生命周期和类相同,在整个应用程序执行期间都有效。

这里要强调一下:

  • static修饰的成员变量和方法,从属于类

  • 普通变量和方法从属于对象

  • 静态方法不能调用非静态成员,编译会报错

用途:

一句话描述就是:方便在没有创建对象的情况下进行调用(方法/变量)。

显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static可以用来修饰类的成员方法、类的成员变量,另外也可以编写static代码块来优化程序性能。

static方法

static方法也成为静态方法,由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有this的,因为不依附于任何对象,既然都没有对象,就谈不上this了,并且由于此特性,在静态方法中不能访问类的非静态成员变量和非静态方法,因为非静态成员变量和非静态方法都必须依赖于具体的对象才能被调用。

所以,如果想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。最常见的静态方法就是main方法,这就是为什么main方法是静态方法就一目了然了,因为程序在执行main方法的时候没有创建任何对象,只有通过类名来访问。

注意:static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大,但是非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力。

static变量

static变量也称为静态变量,静态变量和非静态变量的区别:

  • 静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化

  • 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响

注意:static成员变量初始化顺序按照定义的顺序来进行初始化

1.6.5 synchorized
public static void main(String[] args) {
    testSynchronized();
}

private static void testSynchronized() {
    new Foo().sayHello();
    new Foo().sayGood();
    Foo.sayHi();
}
static class Foo {
    //修饰代码块
    void sayHello() {
        synchronized (this) {
            System.out.println("hello");
    }
}
    //修饰示例方法
    synchronized void sayGood(){
        System.out.println("good");
}
    //修饰静态方法
    static synchronized void sayHi(){
        System.out.println("hi");
    }
}

注意:

(1)修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 (2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作 用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态 资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实 例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允 许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 (3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方 法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态 方法上是给对象实例上锁。

1.6.6 new

创建对象:

Object obj = new Object();

1.6.7 native

Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。可以将native方法比作Java程序同C程序的接口,其实现步骤:   1、在Java中声明native()方法,然后编译;   2、用javah产生一个.h文件;   3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);   4、将第三步的.cpp文件编译成动态链接库文件;   5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

JAVA本地方法适用的情况 1.为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问 2.为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的 3.为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。

1.6.8 volatile [转载自:https://www.cnblogs.com/dolphin0520/p/3920373.html]
  • 一、内存模型相关概念

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

  也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

i = i +1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

  这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

  比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

  可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

  最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

  也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

  为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1)通过在总线加LOCK#锁的方式

  2)通过缓存一致性协议

  这2种方式都是硬件层面上提供的方式。

  在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

  但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

  所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

  • 二、 并发编程中的三个概念

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

1.原子性

  原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  一个很经典的例子就是银行账户转账问题:

  比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

  试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

  所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

  同样地反映到并发编程中会出现什么结果呢?

  举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

i = 9;

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

  那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

2.可见性

  可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;

 假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

  此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

  这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.有序性

  有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;       
boolean flag = false;
i =  1 ;        //语句1 
flag = true;    //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

  下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

  比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

  但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

2 》》1 》》3 》》4

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

  虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

  也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

  • 三、 java内存模型

在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

  在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

  Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

  举个简单的例子:在java中,执行下面这个语句:

i = 10;

  执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

  那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

1.原子性

  在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

  上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

  请分析以下哪些操作是原子性操作:

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

  咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

  语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

  语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

  同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

 所以上面4个语句只有语句1的操作具备原子性。

  也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

  不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

  从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2.可见性

  对于可见性,Java提供了volatile关键字来保证可见性。

  当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性

  在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

  在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

  另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  这8条原则摘自《深入理解Java虚拟机》。

  这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

  下面我们来解释一下前4条规则:

  对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

  第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

  第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  第四条规则实际上就是体现happens-before原则具备传递性。

  • 四、 深入解析volatile关键字

 在前面讲述了很多东西,其实都是为讲述volatile关键字作铺垫,那么接下来我们就进入主题。

1.volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

  先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

2.volatile保证原子性吗?

  从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

  下面看一个例子:

public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
	}
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

  这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

  在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

  把上面的代码改成以下任何一种都可以达到效果:

  采用synchronized:

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3.volatile能保证有序性吗?

  在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  可能上面说的比较绕,举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

 由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

  那么我们回到前面举的一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

  这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

4.volatile的原理和实现机制

  前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

五、 使用volatile关键字的场景

  synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  1)对变量的写操作不依赖于当前值

  2)该变量没有包含在具有其他变量的不变式中

  实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

  事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

  下面列举几个Java中使用volatile的几个场景。

1.状态标记量

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

2.double check

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
1.7 错误处理
1.7.1 try catch finally

如果try中没有异常,则顺序为try→finally,如果try中有异常,则顺序为try→catch→finally。但是当try、catch、finally中加入return之后,就会有几种不同的情况出现,下面分别来说明一下。也可以跳到最后直接看总结。

一、try中带有return

private int testReturn1() {
        int i = 1;
        try {
            i++;
            System.out.println("try:" + i);
            return i;
        } catch (Exception e) {
            i++;
            System.out.println("catch:" + i);
        } finally {
            i++;
            System.out.println("finally:" + i);
        }
        return i;
    }

输出:

try:2
finally:3
2

因为当try中带有return时,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try中计算后的2,而非finally中计算后的3。但有一点需要注意,再看另外一个例子:

private List<Integer> testReturn2() {
    List<Integer> list = new ArrayList<>();
    try {
        list.add(1);
        System.out.println("try:" + list);
        return list;
    } catch (Exception e) {
        list.add(2);
        System.out.println("catch:" + list);
    } finally {
        list.add(3);
        System.out.println("finally:" + list);
    }
    return list;
}

输出:

try:[1]
finally:[1, 3]
[1, 3]

看完这个例子,可能会发现问题,刚提到return时会临时保存需要返回的信息,不受finally中的影响,为什么这里会有变化?其实问题出在参数类型上,上一个例子用的是基本类型,这里用的引用类型。list里存的不是变量本身,而是变量的地址,所以当finally通过地址改变了变量,还是会影响方法返回值的。

二、catch中带有return

private int testReturn3() {
    int i = 1;
    try {
        i++;
        System.out.println("try:" + i);
        int x = i / 0 ;
    } catch (Exception e) {
        i++;
        System.out.println("catch:" + i);
        return i;
    } finally {
        i++;
        System.out.println("finally:" + i);
    }
    return i;
}

输出:

try:2
catch:3
finally:4
3

catch中return与try中一样,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try、catch中累积计算后的3,而非finally中计算后的4。

三、finally中带有return

private int testReturn4() {
        int i = 1;
        try {
            i++;
            System.out.println("try:" + i);
            return i;
        } catch (Exception e) {
            i++;
            System.out.println("catch:" + i);
            return i;
        } finally {
            i++;
            System.out.println("finally:" + i);
            return i;
        }
    }

输出:

try:2
finally:3
3

 当finally中有return的时候,try中的return会失效,在执行完finally的return之后,就不会再执行try中的return。这种写法,编译是可以编译通过的,但是编译器会给予警告,所以不推荐在finally中写return,这会破坏程序的完整性,而且一旦finally里出现异常,会导致catch中的异常被覆盖。

总结:

1、finally中的代码总会被执行。

2、当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。

3、finally中有return时,会直接在finally中退出,导致try、catch中的return失效。

1.7.8 throw

throw是语句抛出一个异常,一般是在代码块的内部,当程序出现某种逻辑错误时由程序员主动抛出某种特定类型的异常

public void getException(){
    String s = "abc";
    if(s.equals("abc")) {
        throw new NumberFormatException();
    } else {
        System.out.println(s);
    }
}

输出:

Exception in thread "main" java.lang.NumberFormatException
1.7.9 throws

当某个方法可能会抛出某种异常时用于throws 声明可能抛出的异常,然后交给上层调用它的方法程序处理

public static void function() throws NumberFormatException {
    String s = "abc";
    System.out.println(Double.parseDouble(s));
}

public static void main(String[] args) {
    try {
        function();
    } catch (NumberFormatException e) {
        System.err.println("非数据类型不能强制类型转换。");
    }
}

输出:

非数据类型不能强制类型转换。

throw与throws的比较:

throws出现在方法函数头;而throw出现在函数体。 throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。 两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

1.8 其他
1.8.1 strictfp

strictfp, 即 strict float point (精确浮点)。   strictfp 关键字可应用于类、接口或方法。使用 strictfp 关键字声明一个方法时,该方法中所有的float和double表达式都严格遵守FP-strict的限制,符合IEEE-754规范。当对一个类或接口使用 strictfp 关键字时,该类中的所有代码,包括嵌套类型中的初始设定值和代码,都将严格地进行计算。严格约束意味着所有表达式的结果都必须是 IEEE 754 算法对操作数预期的结果,以单精度和双精度格式表示。   如果你想让你的浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,可以用关键字strictfp. 用法:

public strictfp class xxxx{
    public static void main(String[] args) {
        float aFloat = 0.6710339f;
        double aDouble = 0.04150553411984792d;
        double sum = aFloat + aDouble;
        float quotient = (float)(aFloat / aDouble);
        System.out.println("float: " + aFloat);
        System.out.println("double: " + aDouble);
        System.out.println("sum: " + sum);
        System.out.println("quotient: " + quotient);
    }
}

输出:

float: 0.6710339
double: 0.04150553411984792
sum: 0.7125394529774224
quotient: 16.167336
1.8.2 transient
class T {  
   transient int a;  //不需要维持  
   int b;  //需要维持  
}  

如果T类的一个对象写入一个持久的存储区域,a的内容不被保存,但b的将被保存。

2. Java基本概念

2.1 理解对象和实例

抽象类是不可以被实例化的,那它的对象就不能叫实例化对象,只能叫对象;

普通类的对象,既可以叫对象,也可以叫实例化对象(实例)。

2.2 重写和重载(override和overload)

重写

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}

class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}

public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
 a.move();// 执行 Animal 类的方法
 
  b.move();//执行 Dog 类的方法
     }
}

输出:

动物可以移动
狗可以跑和走

在上面的例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。

这是由于在编译阶段,只是检查参数的引用类型。

然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。

因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。

思考以下例子:

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}

class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
   public void bark(){
      System.out.println("狗可以吠叫");
   }
}

public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
	  a.move();// 执行 Animal 类的方法
 	  b.move();//执行 Dog 类的方法
 	  b.bark();
    }
}

输出:

TestDog.java:30: cannot find symbol
symbol  : method bark()
location: class Animal
                b.bark();
                 ^

该程序将抛出一个编译错误,因为b的引用类型Animal没有bark方法。

总结:

  • 参数列表必须完全与被重写方法的相同。

  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。

  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。

  • 父类的成员方法只能被它的子类重写。

  • 声明为 final 的方法不能被重写。

  • 声明为 static 的方法不能被重写,但是能够被再次声明。

  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。

  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。

  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。

  • 构造方法不能被重写。

  • 如果不能继承一个方法,则不能重写这个方法。

当需要在子类中调用父类的被重写方法时,要使用 super 关键字。

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}

class Dog extends Animal{
   public void move(){
      super.move(); // 应用super类的方法
      System.out.println("狗可以跑和走");
   }
}

public class TestDog{
   	public static void main(String args[]){
	Animal b = new Dog(); // Dog 对象
  	b.move(); //执行 Dog类的方法
     }
}

输出:

动物可以移动
狗可以跑和走

重载

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);

  • 被重载的方法可以改变返回类型;

  • 被重载的方法可以改变访问修饰符;

  • 被重载的方法可以声明新的或更广的检查异常;

  • 方法能够在同一个类中或者在一个子类中被重载。

  • 无法以返回值类型作为重载函数的区分标准。

示例:

public class Overloading {
    public int test(){
        System.out.println("test1");
        return 1;
    }
    public void test(int a){
        System.out.println("test2");
    }   

    //以下两个参数类型顺序不同
    public String test(int a,String s){
        System.out.println("test3");
        return "returntest3";
    }   

    public String test(String s,int a){
        System.out.println("test4");
        return "returntest4";
    }   

    public static void main(String[] args){
        Overloading o = new Overloading();
        System.out.println(o.test());
        o.test(1);
        System.out.println(o.test(1,"test3"));
            System.out.println(o.test("test4",1));
    }
}

重载和重写的区别:

区别重载重写
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或者删除,一定不能抛出新的或更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)

总结:

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。

  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。

  • (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

2.3 理解和熟练使用内部类

一、 内部类基础

在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。

  • 成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:

class Circle {
    double radius = 0;
    public Circle(double radius) {
        this.radius = radius;
    }

    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
 }

这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
            System.out.println(count);   //外部类的静态成员
        }
    }
 }

不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

外部类.this.成员变量
外部类.this.成员方法

虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

class Circle {
    private double radius = 0;
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
  }

成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
		//第二种方式:
    	Outter.Inner inner1 = outter.getInnerInstance();
		}
	}

        class Outter {
            private Inner inner = null;
            public Outter() {
        }
 
        public Inner getInnerInstance() {
            if(inner == null)
                inner = new Inner();
            return inner;
        }

        class Inner {
            public Inner() {

            }
        }
   }

内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

  • 局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

class People{
    public People() {

    }
}

class Man{
    public Man(){

    }

    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}

注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

  • 匿名内部类

匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码:

scan_bt.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
                // TODO Auto-generated method stub

                }
	});

history_bt.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub

        }
    });

这段代码为两个按钮设置监听器,这里面就使用了匿名内部类。这段代码中的:

new OnClickListener() {

@Override
public void onClick(View v) {
        // TODO Auto-generated method stub

        }
    }

就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。

private void setListener()
    {
        scan_bt.setOnClickListener(new Listener1());
        history_bt.setOnClickListener(new Listener2());
    }

class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub

    }
}

class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub

    }
}

这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。

  匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

  • 静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {

    }

    static class Inner {
        public Inner() {

        }
    }
}
2.4 理解和熟练使用多线程,线程同步

在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。

继承 Thread 实现示例:

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i=0; i<10;i++){
            for (int j=0;j<10;j++){
                System.out.println("子线程:"+i*j);
            }
        }
    }
}

public class Test {
        public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        for (int i=0; i<10;i++){
            for (int j=0;j<10;j++){
                System.out.println("主线程:"+i*j);
            }
        }
    }
}

输出:

在输出中主线程和子线程的输出是无序的。

上述代码中,MyThread类继承了类java.lang.Thread,并覆写了run方法。主线程从main方法开始执行,当主线程执行至t.start()时,启动新线程(注意此处是调用start方法,不是run方法),新线程会并发执行自身的run方法。

实现Runnable 接口实现:

public class Test {
		public static void main(String[] args) {
        Thread t = new Thread(new MyThread2());
        t.start();
        for (int i=0; i<10;i++){
            for (int j=0;j<10;j++){
                System.out.println("主线程:"+i*j);
            }
        }
    }
}

class MyThread2 implements Runnable{
    @Override
    public void run() {
        for (int i=0; i<10;i++){
            for (int j=0;j<10;j++){
                System.out.println("子线程:"+i*j);
            }
        }
    }
}

输出:

在输出中主线程和子线程的输出是无序的。

上述代码中,MyThread类实现了java.lang.Runnable接口,并覆写了run方法,其它与继承java.lang.Thread完全相同。实际上,java.lang.Thread类本身也实现了Runnable接口,只不过java.lang.Thread类的run方法主体里空的,通常被子类覆写(override)。

注:主线程执行完成后,如果还有子线程正在执行,程序也不会结束。只有当所有线程都结束时(不含Daemon Thread),程序才会结束。

Thread 类和 Runnable 接口之间在使用上也是有区别的,如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。

线程的状态变化:

要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:

  • 创建状态

在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。

  • 就绪状态

新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。

  • 运行状态

当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。

  • 阻塞状态

一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  • 死亡状态

线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。

线程操作方法:

  • 线程的强制运行

在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。

class MyThread implements Runnable{ // 实现Runnable接口 
     public void run(){  // 覆写run()方法 
         for(int i=0;i<50;i++){ 
             System.out.println(Thread.currentThread().getName() 
                     + "运行,i = " + i) ;  // 取得当前线程的名字 
         } 
     } 
 }
 public class ThreadJoinDemo{ 
     public static void main(String args[]){ 
         MyThread mt = new MyThread() ;  // 实例化Runnable子类对象 
         Thread t = new Thread(mt,"线程");     // 实例化Thread对象 
         t.start() ; // 启动线程 
         for(int i=0;i<50;i++){ 
             if(i>10){ 
                 try{ 
                     t.join() ;  // 线程强制运行 
                 }catch(InterruptedException e){
                 } 
             } 
             System.out.println("Main线程运行 --> " + i) ; 
         } 
     } 
 }
  • 线程的休眠

在程序中允许一个线程进行暂时的休眠,直接使用 Thread.sleep() 即可实现休眠。

 class MyThread implements Runnable{ // 实现Runnable接口 
     public void run(){  // 覆写run()方法 
         for(int i=0;i<50;i++){ 
             try{ 
                 Thread.sleep(500) ; // 线程休眠 
             }catch(InterruptedException e){
             } 
             System.out.println(Thread.currentThread().getName() 
                     + "运行,i = " + i) ;  // 取得当前线程的名字 
         } 
     } 
 }; 
 public class ThreadSleepDemo{ 
     public static void main(String args[]){ 
         MyThread mt = new MyThread() ;  // 实例化Runnable子类对象 
         Thread t = new Thread(mt,"线程");     // 实例化Thread对象 
         t.start() ; // 启动线程 
     } 
 };
  • 中断线程

当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态。

class MyThread implements Runnable{ // 实现Runnable接口 
     public void run(){  // 覆写run()方法 
         System.out.println("1、进入run()方法") ; 
         try{ 
             Thread.sleep(10000) ;   // 线程休眠10秒 
             System.out.println("2、已经完成了休眠") ; 
         }catch(InterruptedException e){ 
             System.out.println("3、休眠被终止") ; 
             return ; // 返回调用处 
         } 
         System.out.println("4、run()方法正常结束") ; 
     } 
 }; 
 public class ThreadInterruptDemo{ 
     public static void main(String args[]){ 
         MyThread mt = new MyThread() ;  // 实例化Runnable子类对象 
         Thread t = new Thread(mt,"线程");     // 实例化Thread对象 
         t.start() ; // 启动线程 
         try{ 
             Thread.sleep(2000) ;    // 线程休眠2秒 
         }catch(InterruptedException e){ 
             System.out.println("3、休眠被终止") ; 
         } 
         t.interrupt() ; // 中断线程执行 
     } 
 };
  • 后台线程

在 Java 程序中,只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用 setDaemon() 方法即可。

package com.example.myapplication;

public class MyThread02 implements Runnable {
    @Override
    public void run() {
        while(true){
            System.out.println(Thread.currentThread().getName() + "在运行。") ;
        }
    }
}

class ThreadDaemonDemo{
    public static void main(String args[]){
        MyThread02 mt = new MyThread02() ;  // 实例化Runnable子类对象
        Thread t = new Thread(mt,"线程");     // 实例化Thread对象
        t.setDaemon(true) ; // 此线程在后台运行
        t.start() ; // 启动线程
    }
}

在线程类 MyThread 中,尽管 run() 方法中是死循环的方式,但是程序依然可以执行完,因为方法中死循环的线程操作已经设置成后台运行。

  • 线程的优先级

在 Java 的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行。

 class MyThread implements Runnable{ // 实现Runnable接口 
     public void run(){  // 覆写run()方法 
         for(int i=0;i<5;i++){ 
             try{ 
                 Thread.sleep(500) ; // 线程休眠 
             }catch(InterruptedException e){
             } 
             System.out.println(Thread.currentThread().getName() 
                     + "运行,i = " + i) ;  // 取得当前线程的名字 
         } 
     } 
 }; 
 public class ThreadPriorityDemo{ 
     public static void main(String args[]){ 
         Thread t1 = new Thread(new MyThread(),"线程A") ;  // 实例化线程对象 
         Thread t2 = new Thread(new MyThread(),"线程B") ;  // 实例化线程对象 
         Thread t3 = new Thread(new MyThread(),"线程C") ;  // 实例化线程对象 
         t1.setPriority(Thread.MIN_PRIORITY) ;   // 优先级最低 
         t2.setPriority(Thread.MAX_PRIORITY) ;   // 优先级最高 
         t3.setPriority(Thread.NORM_PRIORITY) ;  // 优先级最中等 
         t1.start() ;    // 启动线程 
         t2.start() ;    // 启动线程 
         t3.start() ;    // 启动线程 
     } 
 };
  • 线程 的礼让

在线程操作中,也可以使用 yield() 方法将一个线程的操作暂时让给其他线程执行

 class MyThread implements Runnable{ // 实现Runnable接口 
     public void run(){  // 覆写run()方法 
         for(int i=0;i<5;i++){ 
             try{ 
                 Thread.sleep(500) ; 
             }catch(Exception e){
             } 
             System.out.println(Thread.currentThread().getName() 
                     + "运行,i = " + i) ;  // 取得当前线程的名字 
             if(i==2){ 
                 System.out.print("线程礼让:") ; 
                 Thread.currentThread().yield() ;    // 线程礼让 
             } 
         } 
     } 
 } 
 public class ThreadYieldDemo{ 
     public static void main(String args[]){ 
         MyThread my = new MyThread() ;  // 实例化MyThread对象 
         Thread t1 = new Thread(my,"线程A") ; 
         Thread t2 = new Thread(my,"线程B") ; 
         t1.start() ; 
         t2.start() ; 
     } 
 }

输出:

线程B运行,i = 0
线程A运行,i = 0
线程A运行,i = 1
线程B运行,i = 1
线程A运行,i = 2
线程礼让:线程B运行,i = 2
线程礼让:线程A运行,i = 3
线程B运行,i = 3
线程A运行,i = 4
线程B运行,i = 4

线程同步以及死锁:

一个多线程的程序如果是通过 Runnable 接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。

同步代码块:

synchronized(this){ // 要对当前对象进行同步
    //需要同步的代码块
}
class IsMyThread implements Runnable{
    private int ticket = 100;    // 假设一共有100张票

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            synchronized(this){ // 要对当前对象进行同步
                if(ticket>0){   // 还有票
                    try{
                        Thread.sleep(300) ; // 加入延迟
                    }catch(InterruptedException e){
                        e.printStackTrace() ;
                    }
                    System.out.println(Thread.currentThread().getName()+"卖票:ticket = " + ticket-- );
                }
            }
        }
    }
};
public class SyncDemo{
    public static void main(String args[]){
        IsMyThread mt = new IsMyThread() ;  // 定义线程对象
        Thread t1 = new Thread(mt,"t1:") ;    // 定义Thread对象
        Thread t2 = new Thread(mt,"t2:") ;    // 定义Thread对象
        Thread t3 = new Thread(mt,"t3:") ;    // 定义Thread对象
        t1.start() ;
        t2.start() ;
        t3.start() ;
    }
};

同步方法:

除了可以将需要的代码设置成同步代码块外,也可以使用 synchronized 关键字将一个方法声明为同步方法。

public synchronized void sale(){
	//java代码
}
class IsMyThread implements Runnable{
    private int ticket = 5;    // 假设一共有100张票

    @Override
    public void run() {
        sale();
    }
    public synchronized void sale(){
        for(int i=0;i<100;i++){
            if(ticket>0){   // 还有票
                try{
                    Thread.sleep(300) ; // 加入延迟
                }catch(InterruptedException e){
                    e.printStackTrace() ;
                }
                System.out.println(Thread.currentThread().getName()+"卖票:ticket = " + ticket-- );
            }
        }
    }
}
public class SyncDemo{
    public static void main(String args[]){
        IsMyThread mt = new IsMyThread() ;  // 定义线程对象
        Thread t1 = new Thread(mt,"t1:") ;    // 定义Thread对象
        Thread t2 = new Thread(mt,"t2:") ;    // 定义Thread对象
        Thread t3 = new Thread(mt,"t3:") ;    // 定义Thread对象
        t1.start() ;
        t2.start() ;
        t3.start() ;
    }
}

死锁:

所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。

下面以一个简单范例说明这个概念:

同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。

package com.example.myapplication;

public class DiedProcess implements Runnable{
    private static Zhangsan zs = new Zhangsan() ;       // 实例化static型对象
    private static Lisi ls = new Lisi() ;       // 实例化static型对象
    private boolean flag = false ;  // 声明标志位,判断那个先说话
    @Override
    public void run() {
        if(flag){
            synchronized(zs){   // 同步张三
                zs.say() ;
                try{
                    Thread.sleep(500) ;
                }catch(InterruptedException e){
                    e.printStackTrace() ;
                }
                synchronized(ls){
                    zs.get() ;
                }
            }
        }else{
            synchronized(ls){ //同步李四
                ls.say() ;
                try{
                    Thread.sleep(500) ;
                }catch(InterruptedException e){
                    e.printStackTrace() ;
                }
                synchronized(zs){
                    ls.get() ;
                }
            }
        }
    }

    public static void main(String[] args){
        DiedProcess t1 = new DiedProcess() ;      // 控制张三
        DiedProcess t2 = new DiedProcess() ;      // 控制李四
        t1.flag = true ;
        t2.flag = false ;
        Thread thA = new Thread(t1) ;
        Thread thB = new Thread(t2) ;
        thA.start() ;
        thB.start() ;
    }
}

class Zhangsan{ // 定义张三类
    public void say(){
        System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
    }
    public void get(){
        System.out.println("张三得到画了。") ;
    }
};
class Lisi{ // 定义李四类
    public void say(){
        System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
    }
    public void get(){
        System.out.println("李四得到书了。") ;
    }
};

输出:

张三对李四说:“你给我画,我就把书给你。”
李四对张三说:“你给我书,我就把画给你”
2.5 理解泛型

泛型就是指广泛的、普通的类型。在java中是指把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

示例:

List<Integer> list = new ArrayList();//指定了这个list中的元素必须是int类型
list.add("20");//这里是添加的String类型,编译器会报错
list.add(100);

特性:

泛型只有在编译阶段有效。

泛型的使用:

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
  • 泛型类:

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。泛型类的最基本写法:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

注意:

	1. 泛型的类型参数只能是类类型,不能是简单类型。

	2. 不能对确切的泛型类型使用instanceof操作。
  • 泛型接口:

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

class FruitGenerator1 implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        System.out.println(fruits[rand.nextInt(3)]);
        return fruits[rand.nextInt(3)];
    }

    public static void main(String[] args) {
            FruitGenerator1 f1 = new FruitGenerator1();
            f1.next();
    }
}
  • 泛型方法:

有时候只关心某个方法,那么使用泛型时可以不定义泛型类,而是只定义一个泛型方法,如下:

public <T> void show(T t) {
        System.out.println(t);
    }

需要注意一下定义的格式,泛型必须得先定义才能够使用。

2.6 理解和熟练使用集合:List Set Map Collection

Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象

  • 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。

  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

集合接口描述
Collection Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。 Collection 接口存储一组不唯一,无序的对象。
List List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。 List 接口存储一组不唯一,有序(插入顺序)的对象。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector>
Set Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。 Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>
Map Map 接口存储一组键值对象,提供key(键)到value(值)的映射。
  • ArrayList

public static void main(String[] args) {
    List<String> list=new ArrayList<String>();
    list.add("Hello");
    list.add("World");
    list.add("HAHAHAHA");
    //第一种遍历方法使用 For-Each 遍历 List
    for (String str : list) {            //也可以改写 for(int i=0;i<list.size();i++) 这种形式
        System.out.println(str);
    }

    //第二种遍历,把链表变为数组相关的内容进行遍历
    String[] strArray=new String[list.size()];
    list.toArray(strArray);
    for(int i=0;i<strArray.length;i++) //这里也可以改写为  for(String str:strArray) 这种形式
    {
        System.out.println(strArray[i]);
    }

    //第三种遍历 使用迭代器进行相关遍历

    Iterator<String> ite=list.iterator();
    while(ite.hasNext())//判断下一个元素之后有值
    {
        System.out.println(ite.next());
    }
}

解析:

三种方法都是用来遍历ArrayList集合,第三种方法是采用迭代器的方法,该方法可以不用担心在遍历的过程中会超出集合的长度。

  • Set

set集合添加元素并使用迭代器迭代元素:

public static void main(String[] args) {
        //Set 集合存和取的顺序不一致。
        Set hs = new HashSet();
        hs.add("世界军事");
        hs.add("兵器知识");
        hs.add("舰船知识");
        hs.add("汉和防务");
        System.out.println(hs);
        // [舰船知识, 世界军事, 兵器知识, 汉和防务]
        Iterator it = hs.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }

输出:

[世界军事, 汉和防务, 兵器知识, 舰船知识]
世界军事
汉和防务
兵器知识
舰船知识

HashSet 示例:

使用HashSet存储字符串,并尝试添加重复字符串

回顾String类的equals()、hashCode()两个方法。

    public static void main(String[] args) {
        // Set 集合存和取的顺序不一致。
        Set hs = new HashSet();
        hs.add("世界军事");
        hs.add("兵器知识");
        hs.add("舰船知识");
        hs.add("汉和防务");
        // 返回此 set 中的元素的数量
        System.out.println(hs.size()); // 4
        // 如果此 set 尚未包含指定元素,则返回 true
        boolean add = hs.add("世界军事"); // false
        System.out.println(add);
        // 返回此 set 中的元素的数量
        System.out.println(hs.size());// 4
        Iterator it = hs.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }

使用HashSet存储自定义对象,并尝试添加重复对象(对象的重复的判定)

package com.example.myapplication;

import java.util.HashSet;
import java.util.Iterator;

public class DemoHashSet {
    public static void main(String[] args) {
        HashSet hs = new HashSet();
        hs.add(new Person("jack", 20));
        hs.add(new Person("rose", 20));
        hs.add(new Person("hmm", 20));
        hs.add(new Person("lilei", 20));
        hs.add(new Person("jack", 20));
        Iterator it = hs.iterator();
        while (it.hasNext()) {
            Object next = it.next();
            System.out.println(next);
        }
    }
}

class Person {
    private String name;
    private int age;
    Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public int hashCode() {
        System.out.println("hashCode:" + this.name);
        return this.name.hashCode() + age * 37;
    }
    @Override
    public boolean equals(Object obj) {
        System.out.println(this + "---equals---" + obj);
        if (obj instanceof Person) {
            Person p = (Person) obj;
            return this.name.equals(p.name) && this.age == p.age;
        } else {
            return false;
        }
    }
    @Override
    public String toString() {
        return "Person@name:" + this.name + " age:" + this.age;
    }
}

输出:

  • hashCode:jack
    hashCode:rose
    hashCode:hmm
    hashCode:lilei
    hashCode:jack
    Person@name:jack age:20---equals---Person@name:jack age:20
    Person@name:jack age:20
    Person@name:lilei age:20
    Person@name:rose age:20
    Person@name:hmm age:20
    

    HashSet

    哈希表边存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的所以取数据也是按照哈希值取得。

    HashSet不存入重复元素的规则.使用hashcode和equals

    由于Set集合是不能存入重复元素的集合。那么HashSet也是具备这一特性的。HashSet如何检查重复?HashSet会通过元素的hashcode()和equals方法进行判断元素师否重复。

    当你试图把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较,如果没有相等的hashCode,HashSet就会假设对象没有重复出现。

    简单一句话,如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的。

    因此我们自定义类的时候需要重写hashCode,来确保对象具有相同的hashCode值。

    如果元素(对象)的hashCode值相同,是不是就无法存入HashSet中了? 当然不是,会继续使用equals 进行比较.如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复.新元素可以存入.

    问题:现在有一批数据,要求不能重复存储元素,而且要排序。ArrayList 、 LinkedList不能去除重复数据。HashSet可以去除重复,但是是无序。

    所以这时候就要使用TreeSet了

    示例:

    使用TreeSet集合存储字符串元素,并遍历

    public static void main(String[] args) {
            TreeSet ts = new TreeSet();
            ts.add("ccc");
            ts.add("aaa");
            ts.add("ddd");
            ts.add("bbb");
            System.out.println(ts); // [aaa, bbb, ccc, ddd]
        }
    

    Map

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("1", "value1");
        map.put("2", "value2");
        map.put("3", "value3");
    
        //第一种:普遍使用,二次取值
        System.out.println("通过Map.keySet遍历key和value:");
        for (String key : map.keySet()) {
            System.out.println("key= "+ key + " and value= " + map.get(key));
        }
    
        //第二种
        System.out.println("通过Map.entrySet使用iterator遍历key和value:");
        Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
        }
    
        //第三种:推荐,尤其是容量大时
        System.out.println("通过Map.entrySet遍历key和value");
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
        }
    
        //第四种
        System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
        for (String v : map.values()) {
            System.out.println("value= " + v);
        }
    }
    
    
---| Itreable      接口 实现该接口可以使用增强for循环
                ---| Collection        描述所有集合共性的接口
                    ---| List接口        有序,可以重复,有角标的集合
                            ---| ArrayList   
                            ---|  LinkedList
                    ---| Set接口        无序,不可以重复的集合
                            ---| HashSet  线程不安全,存取速度快。底层是以hash表实现的。
                            ---| TreeSet  红-黑树的数据结构,默认对元素进行自然排序(String)。
2.7 了解反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

用途:

在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。

反射机制相关的类:

类名用途
Class 代表类的实体,在运行的java应用程序中表示类和接口
Field 代表类的成员变量(成员变量也称为类的属性)
Method 代表类的方法
Constructor 代表类的构造方法

Class 类:

代表类的实体,在运行的Java应用程序中表示类和接口。在这个类中提供了很多有用的方法,这里对他们简单的分类介绍

  • 获得类相关的方法

方法用途
asSubclass(Class<U> clazz) 把传递的类的对象转换成代表其子类的对象
Cast 把对象转换成代表类或是接口的对象
getClassLoader() 获得类的加载器
getClasses() 返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses() 返回一个数组,数组中包含该类中所有类和接口类的对象
forName(String className) 根据类名返回类的对象
getName() 获得类的完整路径名字
newInstance() 创建类的实例
getPackage() 获得类的包
getSimpleName() 获得类的名字
getSuperclass() 获得当前类继承的父类的名字
getInterfaces() 获得当前类实现的类或是接口
  • 获得类中属性相关的方法

方法用途
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对象
getDeclaredFields() 获得所有属性对象
  • 获得类中注解相关的方法

方法用途
getAnnotation(Class<A> annotationClass) 返回该类中与参数类型匹配的公有注解对象
getAnnotations() 返回该类所有的公有注解对象
getDeclaredAnnotation(Class<A> annotationClass) 返回该类中与参数类型匹配的所有注解对象
getDeclaredAnnotations() 返回该类所有的注解对象
  • 获得类中构造器相关的方法

方法用途
getConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法
  • 获得类中方法相关的方法

方法用途
getMethod(String name, Class...<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法
  • 类中其他重要的方法

方法用途
isAnnotation() 如果是注解类型则返回true
isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果是指定类型注解类型则返回true
isAnonymousClass() 如果是匿名类则返回true
isArray() 如果是一个数组类则返回true
isEnum() 如果是枚举类则返回true
isInstance(Object obj) 如果obj是该类的实例则返回true
isInterface() 如果是接口类则返回true
isLocalClass() 如果是局部类则返回true
isMemberClass() 如果是内部类则返回true

Field类

代表类的成员变量(成员变量也称为类的属性)。

方法用途
equals(Object obj) 属性与obj相等则返回true
get(Object obj) 获得obj中对应的属性值
set(Object obj, Object value) 设置obj中对应属性值

Method类

代表类的方法。

方法用途
invoke(Object obj, Object... args) 传递object对象及参数调用该对象对应的方法

Constructor类

代表类的构造方法。

方法用途
newInstance(Object... initargs) 根据传递的参数创建类的对象

详情见:https://www.jianshu.com/p/9be58ee20dee

2.8 理解 转型、装箱、拆箱 概念
  • 转型:

父类引用指向子类对象。

什么叫父类引用指向子类对象?

从 2 个名词开始说起:向上转型(upcasting)向下转型(downcasting)

举个例子:有2个类,Father 是父类,Son 类继承自 Father。

示例1:

Father f1 = new Son();   // 这就叫 upcasting (向上转型)
// 现在 f1 引用指向一个Son对象

Son s1 = (Son)f1;   // 这就叫 downcasting (向下转型)
// 现在f1 还是指向 Son对象

示例2:

Father f2 = new Father();
Son s2 = (Son)f2;       // 出错,子类引用不能指向父类对象

你或许会问,第1个例子中:Son s1 = (Son)f1; 问为什么是正确的呢。

很简单因为 f1 指向一个子类对象,Father f1 = new Son(); 子类 s1 引用当然可以指向子类对象了。

而 f2 被传给了一个 Father 对象,Father f2 = new Father(); 子类 s2 引用不能指向父类对象。

总结:

1、父类引用指向子类对象,而子类引用不能指向父类对象。

2、把子类对象直接赋给父类引用叫upcasting向上转型,向上转型不用强制转换,如:

Father f1 = new Son();

3、把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要强制转换,如:

f1 就是一个指向子类对象的父类引用。把f1赋给子类引用 s1 即 Son s1 = (Son)f1;

其中 f1 前面的(Son)必须加上,进行强制转换。

  • 装箱拆箱

一、什么是装箱和拆箱

Java为每种基本数据类型都提供了对应的包装器类型在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:

Integer i = new Integer(10);

而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:

Integer i = 10;

这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。

那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:

Integer i = 10; //装箱
int n = i;  //拆箱

  简单一点说,装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。

下表是基本数据类型对应的包装器类型:

基本数据类型包装器类型
int(4字节) Integer
byte(1字节) Byte
short(2字节) Short
long(8字节) Long
float(4字节) Float
double(8字节) Double
char(2字节) Character
boolean(未定) Boolean
2.9 理解面向接口编程,多态

示例:

已知要实现U盘、MP3播放器、移动硬盘三种移动存储设备,要求计算机能同这三种设备进行数据交换,并且以后可能会有新的第三方的移动存储设备,所以计算机必须有扩展性,能与目前未知而以后可能会出现的存储设备进行数据交换。各个存储设备间读、写的实现方法不同,U盘和移动硬盘只有这两个方法,MP3Player还有一个PlayMusic方法。

IMobileStorage接口:只定义了读和写的方法

package com.example.myapplication.InterfaceTest;

public interface IMobileStorage {
    void read();
    void write();
}

MobileHardDisk类:

package com.example.myapplication.InterfaceTest;

public class MobileHardDisk implements IMobileStorage {
    @Override
    public void read() {
        System.out.println("Reading from MobileHardDisk……");
        System.out.println("Read finished!");
    }

    @Override
    public void write() {
        System.out.println("Writing to MobileHardDisk……");
        System.out.println("Write finished!");
    }
}

MP3Player类:

package com.example.myapplication.InterfaceTest;

public class MP3Player implements IMobileStorage {

    @Override
    public void read() {
        System.out.println("Reading from MP3Player……");
        System.out.println("Read finished!");
    }

    @Override
    public void write() {
        System.out.println("Writing to MP3Player……");
        System.out.println("Write finished!");
    }

    public void PlayMusic(){
        System.out.println("Music is playing……");
    }
}

FlashDisk类:

package com.example.myapplication.InterfaceTest;

public class FlashDisk implements IMobileStorage {
    @Override
    public void read() {
        System.out.println("Reading from FlashDisk……");
        System.out.println("Read finished!");
    }

    @Override
    public void write() {
        System.out.println("Writing to FlashDisk……");
        System.out.println("Write finished!");
    }
}

Computer类:通过Computer 来调用具体实例的读写方法

package com.example.myapplication.InterfaceTest;

public class Computer  {
    private IMobileStorage _usbDrive;

    public IMobileStorage get_usbDrive() {
        return _usbDrive;
    }

    public void set_usbDrive(IMobileStorage _usbDrive) {
        this._usbDrive = _usbDrive;
    }

    public Computer(){}

    public Computer(IMobileStorage _usbDrive) {
        this._usbDrive = _usbDrive;
    }

    public void ReadData(){
        this._usbDrive.read();
    }

    public void WriteData(){
        this._usbDrive.write();
    }
}

NewMoblieStorage类:当新的存储方式出现,仍然实现了IMobileStorage接口,所以使用的时候都是一样的,插入到"USB "上

package com.example.myapplication.InterfaceTest;

class NewMoblieStorage implements IMobileStorage {
    @Override
    public void read() {
        System.out.println("NewMoblieStorage read ok");
    }

    @Override
    public void write() {
        System.out.println("NewMoblieStorage write ok");
    }
}

NewStorageInterface接口:出现了新的”USB“接口标准

package com.example.myapplication.InterfaceTest;

public interface NewStorageInterface {
    void rd();
    void wt();
}

SuperStorage类:使用的是NewStorageInterface的接口,所以这个设备不能插入到原先的电脑的USB 接口中

package com.example.myapplication.InterfaceTest;

public class SuperStorage implements NewStorageInterface {
    @Override
    public void rd() {
        System.out.println("SuperStorage read ok!");
    }

    @Override
    public void wt() {
        System.out.println("SuperStorage write ok!");
    }
}

SuperStorageAdapter类:那么就使用一个转接口,使得新的设备也能够在电脑上使用

package com.example.myapplication.InterfaceTest;

public class SuperStorageAdapter implements IMobileStorage {
    private SuperStorage _superstorage;

    public SuperStorage get_superstorage() {
        return _superstorage;
    }

    public void set_superstorage(SuperStorage _superstorage) {
        this._superstorage = _superstorage;
    }

    @Override
    public void read() {
        System.out.println("SuperStorageAdapter read ok!");
    }

    @Override
    public void write() {
        System.out.println("SuperStorageAdapter write ok!");
    }
}

TestInterface执行类:

package com.example.myapplication.InterfaceTest;

public class TestInterface {
    public static void main(String[] args){
        Computer computer = new Computer();
        IMobileStorage mp3Player = new MP3Player();
        IMobileStorage flashDisk = new FlashDisk();
        IMobileStorage moblieHardDisk = new MobileHardDisk();

        System.out.println("I inserted my MP3 Player into my computer and copy some music to it:");
        computer.set_usbDrive(mp3Player);
        computer.WriteData();
        System.out.println("====================");

        System.out.println("Well,I also want to copy a great movie to my computer from a mobile hard disk:");
        computer.set_usbDrive(moblieHardDisk);
        computer.ReadData();
        System.out.println("====================");

        System.out.println("OK!I have to read some files from my flash disk and copy another file to it:");
        computer.set_usbDrive(flashDisk);
        computer.ReadData();
        computer.WriteData();
        System.out.println();

        IMobileStorage newMobileStorage = new NewMoblieStorage();
        computer.set_usbDrive(newMobileStorage);
        newMobileStorage.write();
        newMobileStorage.read();

        SuperStorageAdapter superStorageAdapter = new SuperStorageAdapter();
        SuperStorage superStorage = new SuperStorage();
        superStorageAdapter.set_superstorage(superStorage);

        System.out.println("Now,I am testing the new super storage with adapter:");
        computer.set_usbDrive(superStorageAdapter);
        computer.ReadData();
        computer.WriteData();
        System.out.println();
    }
}

输出:

I inserted my MP3 Player into my computer and copy some music to it:
Writing to MP3Player……

Write finished!

Well,I also want to copy a great movie to my computer from a mobile hard disk:
Reading from MobileHardDisk……

Read finished!

OK!I have to read some files from my flash disk and copy another file to it:
Reading from FlashDisk……
Read finished!
Writing to FlashDisk……
Write finished!

NewMoblieStorage write ok
NewMoblieStorage read ok
Now,I am testing the new super storage with adapter:
SuperStorageAdapter read ok!
SuperStorageAdapter write ok!

3. 设计模式和算法

3.1 设计模式
3.1.1 工厂模式

简介:

目前从网上了解工厂模式大体分为简单工厂、工厂方法、抽象工厂等三种模式。工厂方法模式也可称为工厂模式,与抽象模式都是属于GOF23种设计模式中的一员。可以大概理解为:简单工厂进阶变成了工厂方法,然后再进阶成了抽象工厂。难度逐步增加,也越来越抽象。

一、 简单工厂

属于创建型模式,又叫做静态工厂方法模式,不属于23种GOF设计模式之一。是由一个工厂对象决定创建出哪一种产品类的实例。实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。

主要角色 工厂:负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。 抽象产品:负责描述产品的公共接口 具体产品:描述生产的具体产品。 举个简单易懂的例子: “假设”有一台饮料机(工厂),可以调出各种口味的饮料(抽象产品),有三个按钮(参数)对应这三种饮料(具体产品)。这时候你可以根据点击按钮来选择你喜欢的饮料。

/**
 *  @ Product.java
 *  抽象产品
 *  描述产品的公共接口
 */
abstract  class Product {
    //产品介绍
    abstract void intro();
}

/**
 * @ AProduct.java
 * 具体产品A
 * (可以看成是一种饮料:可乐)
 */
public class AProduct extends Product{
    @Override
    void intro() {
        System.out.println("可乐");
    }
}

/**
 * @ BProduct.java
 * @具体产品B
 * @(可以看成是一种饮料:奶茶)
 */
public class BProduct extends Product{
    @Override
    void intro() {
        System.out.println("奶茶");
    }
}

/**
 * @ CProduct.java
 * 具体产品C
 * (可以看成是一种饮料:咖啡)
 */
public class CProduct extends Product{
    @Override
    void intro() {
        System.out.println("咖啡");
    }
}
/**
 * 工厂
 * 负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。
 */
public class Factory {
    /**
     * 供外界调用的方法
     * (可以看成是对外提供的三种按钮)
     * @param type 
     * @return 产品实例
     */
    public static Product getProduct(String type) {
        switch (type) {
            case "A":
                return new AProduct();
            case "B":
                return new BProduct();
            case "C":
                return new CProduct();
            default:
                return null;
        }
    }
}
public class Test {
    public static void main(String[] args) {
        //创建具体的工厂
        Factory factory = new Factory();
        //根据传入的参数生产不同的产品实例
        //(按下不同的按钮,获取饮料)
        Product A = Factory.getProduct("A");
        A.intro();
        Product B = Factory.getProduct("B");
        B.intro();
        Product C = Factory.getProduct("C");
        C.intro();
    }
}

输出:

可乐
奶茶
咖啡

根据例子可以描述为:一个抽象产品类,可以派生出多个具体产品类。一个具体工厂类,通过往此工厂的static方法中传入不同参数,产出不同的具体产品类实例。

优点:将创建使用工作分开,不必关心类对象如何创建,实现了解耦; 缺点:违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。

二、 工厂方法模式

又称工厂模式、多态工厂模式虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。一种常用的对象创建型设计模式,此模式的核心精神是封装类中不变的部分。

作用:将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类。

主要角色 抽象工厂:描述具体工厂的公共接口 具体工厂:描述具体工厂,创建产品的实例,供外界调用 抽象产品:负责描述产品的公共接口 具体产品:描述生产的具体产品

产品:

/**
 * @ Product.java 
 *   抽象产品
 */
abstract class Product {
    //产品介绍
    abstract void intro();
}

/**
 * @ ProductA.java 
 * 具体产品A
 */
public class ProductA extends Product{
    @Override
    void intro() {
        System.out.println("饮料A");
    }
}

/**
 * @ ProductB.java 
 * 具体产品B
 */
public class ProductB extends Product{
    @Override
    void intro() {
        System.out.println("饮料B");
    }
}

工厂:

/**
 *  @ Factory.java
 *    抽象工厂
 */
abstract class Factory {
    //生产产品
    abstract Product getProduct();
}

/**
 * @ FactoryA.java
 * 具体工厂A
 * 负责具体的产品A生产
 */
public class FactoryA extends Factory{
    @Override
    Product getProduct() {
        return new ProductA();
    }
}

/**
 * @ FactoryB.java
 * @具体工厂B
 * 负责具体的产品B生产
 */
public class FactoryB extends Factory{
    @Override
    Product getProduct() {
        return new ProductB();
    }
}
public class Test {
    public static void main(String[] args) {
        //创建具体的工厂
        FactoryA factoryA = new FactoryA();
        //生产相对应的产品
        factoryA.getProduct().intro();
        FactoryB factoryB = new FactoryB();
        factoryB.getProduct().intro();
    }
}

根据例子可以描述为:一个抽象产品类,可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类只能创建一个具体产品类的实例

优点:

  1. 符合开-闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可

  2. 符合单一职责原则:每个具体工厂类只负责创建对应的产品

缺点:

  1. 增加了系统的复杂度:类的个数将成对增加(一种产品就需要增加一种工厂)

  2. 增加了系统的抽象性和理解难度

  3. 一个具体工厂只能创建一种具体产品

三、 抽象工厂模式

定义:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。 解决的问题:每个工厂只能创建一类产品(工厂方法模式)

抽象工厂模式与工厂方法模式最大的区别:抽象工厂中每个工厂可以创建多种类的产品;而工厂方法每个工厂只能创建一类

主要对象 抽象工厂:描述具体工厂的公共接口 具体工厂:描述具体工厂,创建产品的实例,供外界调用 抽象产品族:描述抽象产品的公共接口 抽象产品:描述具体产品的公共接口 具体产品:具体产品

产品:

/**
 * @ Product.java
 * 抽象产品族 (食品)
 */
abstract class Product {
    //产品介绍
    abstract void intro();
}

/**
 * @ ProductA.java
 * 抽象产品  (饮料)
 */
abstract class ProductA extends Product{
    @Override
    abstract void intro();
}

/**
 * @ ProductB.java
 * 抽象产品  (零食)
 */
abstract class ProductB extends Product{
    @Override
    abstract void intro();
}

/**
 * @ ProductAa.java
 * 具体产品  (矿泉水)
 */
public  class ProductAa extends ProductA{
    @Override
    void intro() {
        System.out.println("矿泉水");
    }
}

/**
 * @ ProductBb.java
 * 抽象产品  (面包)
 */
public class ProductBb extends ProductB{
    @Override
    void intro() {
        System.out.println("面包");
    }
}

工厂:

/**
 * @ Factory.java
 * 抽象工厂
 */
abstract class Factory {
    //生产饮料
    abstract Product getProductA();
    //生产零食
    abstract Product getProductB();
}

/**
 * @ FactoryA.java
 * 具体工厂A
 * 负责具体的A类产品生产
 */
public class FactoryA extends Factory{
    @Override
    Product getProductA() {
        //生产矿泉水
        return new ProductAa();
    }
    @Override
    Product getProductB() {
        //生产面包
        return new ProductBb();
    }
}
public class Test {
    public static void main(String[] args) {
        //创建零食售卖机(具体工厂),
        FactoryA factoryA = new FactoryA();
        //获取矿泉水与面包(具体产品)
        factoryA.getProductA().intro();
        factoryA.getProductB().intro();
    }
}

输出:

矿泉水
面包

根据实例可以描述为: 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。 每个具体工厂类可以创建多个具体产品类的实例。.

优点:

  1. 降低耦合

  2. 符合开-闭原则

  3. 符合单一职责原则

  4. 不使用静态工厂方法,可以形成基于继承的等级结构。

缺点:难以扩展新种类产品

总结:

角色不同:

  1. 简单工厂:具体工厂、抽象产品、具体产品

  2. 工厂方法:抽象工厂、具体工厂、抽象产品、具体产品

  3. 抽象工厂:抽象工厂、具体工厂、抽象产品族、抽象产品、具体产品

定义:

  1. 简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(继承自一个父类或接口)的实例。

  2. 工厂方法:定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象

  3. 抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。

对比:

  1. 工厂方法模式解决了简单工厂模式的“开放 - 关闭原则

  2. 抽象工厂模式解决了工厂方法模式一个具体工厂只能创建一类产品

3.1.2 代理模式

简介:

代理也称“委托”,分为静态代理和动态代理,代理模式也是常用的设计模式之一,具有方法增强、高扩展性的设计优势。

代理的设计理念是限制对象的直接访问,即不能通过 new 的方式得到想要的对象,而是访问该对象的代理类。

这样的话,我们就保护了内部对象,如果有一天内部对象因为某个原因换了个名或者换了个方法字段等等,那对访问者来说一点不影响,因为他拿到的只是代理类而已,从而使该访问对象具有高扩展性。

代理模式有两种实现方式,静态代理和动态代理。

代理模式涉及的角色:

抽象角色:为真实对象和代理对象提供一个共同的接口,一般是抽象类或者接口。

代理角色:代理角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能够代替真实对象。同时,代理对象可以在执行真实对象的操作时,附加其他操作,相当于对真实对象的功能进行拓展。

真实角色:最终引用的对象。

一、 静态代理:

代理类在程序运行前就已经存在,那么这种代理方式被称为静态代理

  • 定义抽象角色

/**
 * 定义一个产家,提供卖货的功能
 **/
public interface Producer {
    void sell();
}
  • 定义一个真实角色

/**
 * 定义一个小卖部,帮产家卖货
 **/
public class Canteen implements Producer {
    @Override
    public void sell() {
        System.out.println("小卖部进行卖货");
    }
}
  • 定义一个代理类

/**
 * 定义产家的代理商,也具备卖货的功能
 **/
public class ProducerProxy implements Producer {
    private Producer producer;

    ProducerProxy(Producer producer) {
        this.producer = producer;
    }

    @Override
    public void sell() {
        System.out.println("--------小卖部卖货前--------");
        producer.sell();
        System.out.println("--------小卖部卖货后--------");
    }
}

class StaticProxyTest {
    public static void main(String[] args) {
        Producer producer = new Canteen();
        ProducerProxy personProxy = new ProducerProxy(producer);
        personProxy.sell();
    }
}

二、 动态代理

代理类在程序运行时创建的代理方式被称为 动态代理,如果目标对象实现了接口,采用JDK的动态代理。

  • 定义一个产家

/**
 * 定义一个产家
 **/
public interface Producer2 {
    void sell();
}
  • 定义一个真实角色

/**
 * 定义商家
 **/
public class Canteen2 implements Producer2 {
    @Override
    public void sell() {
        System.out.println("小卖部进行卖货");
    }
}
  • 实现代理

public class Producer2Proxy {
    public static void main(String[] args) {
        Producer2 producer2 = new Canteen2();
        Producer2 producerProxy = (Producer2) Proxy.newProxyInstance(producer2.getClass().getClassLoader(),
                producer2.getClass().getInterfaces(), (proxy, method, args1) -> {
                    System.out.println("----------小卖部卖货前--------");
                    Object invoke = method.invoke(producer2,args1);
                    System.out.println("----------小卖部卖货后--------");
                    return invoke;
                });
        producerProxy.sell();
    }
}

总结:

1、静态代理:

可以做到在不修改目标对象的功能前提下,对目标功能扩展

缺点

代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护

2、JDK动态代理

代理对象不需要实现接口, 利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)

缺点

目标对象一定要实现接口,否则不能用动态代理

什么时候使用代理模式

1、当我们想要隐藏某个类时,可以为其提供代理。

2、当一个类需要对不同的调用者提供不同的调用权限时,可以使用代理类来实现(代理类不一定只有一个,我们可以建立多个代理类来实现,也可以在一个代理类中进行权限判断来进行不同权限的功能调用)。

3、当我们要扩展某个类的某个功能时,可以使用代理模式,在代理类中进行简单扩展(只针对简单扩展,可在引用委托类的语句之前与之后进行)。

3.1.3 策略模式

简介:

定义了算法家族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化不会影响到使用算法的用户

在策略模式中,可以在运行时更改类行为或其算法。 这种类型的设计模式属于行为模式。

在策略模式中,创建表示各种策略对象和其行为根据其策略对象而变化的上下文对象。 策略对象更改上下文对象的执行算法。

使用场景:

系统有很多类,而他们的区别仅仅在于他们的行为不同。

一个系统需要动态地在集中算法中选择一种

示例:

在这个示例中,将创建一个 Strategy 接口,定义实现策略接口的操作和具体策略类。 上下文类- Context 是使用策略的类。

StrategyPatternDemo是一个演示类,将使用上下文- Context 和策略对象来演示上下文行为基于其部署或使用的策略的变化。

  • 创建一个类接口,其代码如下 :

//Strategy.java
public interface Strategy {
   public int doOperation(int num1, int num2);
}
  • 创建三个实现相同接口的具体类。其代码分别如下 -

//OperationAdd.java
public class OperationAdd implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 + num2;
   }
}

//OperationSubstract.java
public class OperationSubstract implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 - num2;
   }
}

//OperationMultiply.java
public class OperationMultiply implements Strategy{
   @Override
   public int doOperation(int num1, int num2) {
      return num1 * num2;
   }
}

创建上下文(Context )类

//Context.java
public class Context {
   private Strategy strategy;

   public Context(Strategy strategy){
      this.strategy = strategy;
   }

   public int executeStrategy(int num1, int num2){
      return strategy.doOperation(num1, num2);
   }
}

使用上下文- Context 在更改其策略时查看行为更改。

public class StrategyPatternDemo {
   public static void main(String[] args) {
      Context context = new Context(new OperationAdd());
      System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationSubstract());
      System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationMultiply());
      System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
   }
}

输出:

10 + 5 = 15
10 - 5 = 5
10 * 5 = 50

总结:

优点:

1.开闭原则

2.避免使用多重条件转义语句

3.提高算法的保密性和安全性

缺点:

客户端必须知道所有的策略类,并自行决定使用哪一个策略类

产生了很多策略类

3.1.4 迭代器模式

简介:

迭代器(Iterator)模式又叫游标(Cursor)模式,通常用于集合类型来提供一种顺序访问其中元素而又不必暴漏集合的内部结构,是一种行为模式。

关于迭代器(Iterator)我想对Java Collection有过接触的同学就不陌生,所以本文也就无需举其他例子了,看一下在Java SDK中是如何实现的就好了。

据统计,java.util.ArrayList是Java SDK中使用频率最高的类。有人说程序就是数据结构+算法,可见数据结构的重要性。我们在日常开发中,时常跟Java集合中的各种工具类打交道,对于它们,遍历元素又是家常便饭,比如:

String[] strings = new String[]{"Hello,", "Java", "Design", "Patterns."};
List<String> stringList = Arrays.asList(strings);
Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
}

输出:

Hello, Java Design Patterns. 

其中的Iterator就是迭代器,它有两个核心方法:

public interface Iterator<E> {
    boolean hasNext();//用于判断是否还有下一个元素
    E next();//用于返回下一个元素,同时“看向”这个元素的再下一个元素
}

Collection是继承自Iterable,而后者的核心方法就是返回Iterator实例的iterator()方法:

public interface Iterable<T> {
    Iterator<T> iterator();
}

所以我们平时使用的各种不同的ListSetQueue的具体实现,都能返回迭代器以便能够对它们中的元素进行遍历。

java.util.ArrayList为例,它的iterator()方法返回的是Iterator的其内部类的实现:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    // 实际存储元素的数据
    transient Object[] elementData; 
    // 元素实际个数
    private int size;
    
    ... ...
    
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        ... ...

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            ... ...
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        ... ...
    } //End of Itr
    ... ...
} //End of ArrayList

其中去掉了一些代码。ArrayList是一种数组类型的List,其内部采用一个Object[]来保存所有元素,size用来保存一共有多少个元素。

方法iterator()会返回一个内部类Itr,后者实现了Iterator接口的hasNext()next()方法。

既然是迭代遍历,那么就需要有一个变量能够记录遍历到哪个元素了,这里Itr.cursor就是用来记录迭代索引的变量。每次调用hasNext()判断后边是否还有元素的时候,其实就是比较这个索引的值是否和size相等;每次调用next()返回下一个元素,其实就是返回elementData[cursor]并让cursor自增以指向下一个元素。

这就是迭代器模式,如果去掉各种接口和类的继承关系,简单来说:

迭代器模式是为集合类的事物服务的,因此类关系就很好说了:一边是集合,一边是迭代器,集合能够返回迭代器,迭代器能够遍历集合。 出于面向接口的更加灵活的模式设计,集合和迭代器均有抽象层(接口或抽象类)以及具体实现类。

迭代器模式的典型用法:

Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
}

iterator可能是一个ArrayList返回的,可能是一个HashSet返回的,我们都不care,只要获取到迭代器,我们就可以“无脑流”一路hasNext() + next(),这就是迭代器的初衷,它为集合封装了迭代遍历元素的方法,留给用户的是一套简单易用的遍历接口。

3.1.5 观察者模式

简介:

1、属于行为型模式:这些设计模式特别关注对象之间的通信。

2、当对象间存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知它的依赖对象。

3、意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

4、主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

5、何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

6、使用场景

(1)一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。

(2)一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。

(3)一个对象必须通知其他对象,而并不知道这些对象是谁。

(4)需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

7、注意事项

(1)JAVA 中已经有了对观察者模式的支持类。

(2)避免循环引用。

(3)如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

实现方式:

实现观察者模式有很多形式,比较直观的一种是使用一种“注册——通知——注销”的形式。

比如Android中的广播。。。。。

观察者模式主要角色

抽象观察者:描述观察者的公共接口(收到消息的方法),也可以定义为接口(interface)。

具体观察者:描述具体观察者并对观察目标的改变做出反应。

抽象被观察者(目标):是指被观察的对象,描述被观察者的公共接口(比如通知、注册、注销等),也可以定义为接口

具体被观察者(具体目标):描述具体的被观察者,与观察者建立联系。当状态发生变化时,通知观察者。

举例:

例如我们知道Android广播的大致实现流程:创建接收者(具体观察者)继承广播(抽象观察者),然后注册广播(绑定观察者,建立联系)。广播发送者(具体的被观察者)发送广播通知。最后是注销广播(解除绑定,断开联系)。

抽象 被观察者:Observable.java;具体 被观察者:SchoolsBroadcast.java

/**
 * Observable.java
 *  抽象被观察者
 */
abstract class Observable {
     //发送广播
    abstract void sendBroadcast(String message);
}

/**
 * SchoolsBroadcast.java
 *  具体的 被观察者(学校广播)
 */
public class SchoolsBroadcast extends Observable{
    //用来存储观察者
    private List<Observer> observers = new ArrayList<Observer>();
    @Override
    void sendBroadcast(String message) {
        System.out.println("学校发出通知:"+message);
        for(Observer ob:observers) {
            ob.receive(message);
        }
    }
    //绑定观察者(可以移动到抽象被观察者中)
    public void registerReceiver(Observer observer) {
        observers.add(observer);
    }
    //解绑观察者(可以移动到抽象被观察者中)
    public void unRegisterReceiver(Observer observer) {
        if(observers.contains(observer)) {
            observers.remove(observer);
        }
    }
}

抽象 观察者:Observer.java;具体 观察者:StudentA.java、StudentB.java、StudentC.java

/**
 * Observer.java
 *  抽象观察者
 */
abstract class Observer {
    //收到通知
    abstract void receive(String message);
}

/**
 *  StudentA.java
 *  具体的观察者(学生A)
 */
public class StudentA extends Observer{
    @Override
    void receive(String message) {
        System.out.println("学生A收到消息:"+message);
    }
}

/**
 *  StudentB.java
 *  具体的观察者(学生B)
 */
public class StudentB extends Observer{
    @Override
    void receive(String message) {
        System.out.println("学生B收到消息:"+message);
    }
}

/**
 *  StudentC.java
 *  具体的观察者(学生C)
 */
public class StudentC extends Observer{
    @Override
    void receive(String message) {
        System.out.println("学生C收到消息:"+message);
    }
}
public class Test {
    public static void main(String[] args) {
        //创建被观察者(学校广播)
        SchoolsBroadcast schoolsBroadcast = new SchoolsBroadcast();
        //创建观察者(学生)
        StudentA studentA = new StudentA();
        //绑定观察者 建立联系
        schoolsBroadcast.registerReceiver(studentA);
        schoolsBroadcast.registerReceiver(new StudentB());
        schoolsBroadcast.registerReceiver(new StudentC());
        //被观察者(学校广播)发出通知
        schoolsBroadcast.sendBroadcast("放学");
        System.out.println("=====================");
        //解绑观察者(学生A)
        schoolsBroadcast.unRegisterReceiver(studentA);
        //被观察者(学校广播)发出通知
        schoolsBroadcast.sendBroadcast("学生A请假");
    }
}

输出:

学校发出通知:放学
学生A收到消息:放学
学生B收到消息:放学
学生C收到消息:放学
=====================
学校发出通知:学生A请假
学生B收到消息:学生A请假
学生C收到消息:学生A请假

总结:

优点:

1、观察者和被观察者是抽象耦合的。

2、建立一套触发机制。.

3、符合“开闭原则”的要求。

缺点:

1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

3.1.6 责任链模式

简介:

责任链模式(Chain of Responsibility Pattern)中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。这种类型的设计模式属于行为型模式。

责任链模型关注于单个环节,而不是整体流程

示例:

某公司员工要出差,出差这笔费用需要向上级汇报并申请,但是针对出差费过多的情况,可能会出现这位员工的直属上级没有权限批准,可能这位直属领导会向他的上级进行汇报申请,这样依次类推直到某个领导能有权限批准这笔费用为止!

员工出差的类:

/**
 * 员工A要出差
 */
public class Person {

    private int money;

    /**
     * 设置出差需要非费用
     */
    public void setMoney(int money) {
        this.money = money;
    }

    /**
     * 获取需要申请的出差费用
     */
    public int getMoney() {
        return money;
    }

    /**
     * 向上级回报
     */
    public void getApply() {
        System.out.print("老大我需要的出差费用是" + getMoney());
    }

}

他的上级依次是只能审批500的出差费用,审批1000的,审批1500的,审批2000的。

/**
 * 申请500已下的金额可以批,大于500会向上级回报
 */
public class Leader500 {
    public void handler(Person person) {
        person.getApply();
        System.out.print("     我是Leader500  你需要申请的" + person.getMoney() + "我批准了");
    }
}

/**
 * 申请1000已下的金额可以批,大于1000会向上级回报
 */
public class Leader1000 {
    public void handler(Person person) {
        person.getApply();
        System.out.print("     我是Leader1000  你需要申请的" + person.getMoney() + "我批准了");
    }
}

/**
 * 申请1500已下的金额可以批,大于1500会向上级回报
 */
public class Leader1500 {
    public void handler(Person person) {
        person.getApply();
        System.out.print("     我是Leader1500  你需要申请的" + person.getMoney() + "我批准了");
    }
}

/**
 * 申请2000已下的金额可以批,大于2000会向上级回报
 */
public class Leader2000 {
    public void handler(Person person) {
        person.getApply();
        System.out.print("     我是Leader2000  你需要申请的" + person.getMoney() + "我批准了");
    }
}

员工做一次简单的申请会进行这样的操作:

  public static void main(String[] args) {

        //领导们
        Leader500 leader500 = new Leader500();
        Leader1000 leader1000 = new Leader1000();
        Leader1500 leader1500 = new Leader1500();
        Leader2000 leader2000 = new Leader2000();

        //申请人
        Person person = new Person();
        person.setMoney(2000);


        if (person.getMoney() <= 500) {
            leader500.handler(person);
        } else if (person.getMoney() <= 1000) {
            leader1000.handler(person);
        } else if (person.getMoney() <= 1500) {
            leader1500.handler(person);
        } else if (person.getMoney() <= 2000) {
            leader2000.handler(person);
    	}
      
  }

当申请金额为2000时得到的结果是:

老大我需要的出差费用是2000     我是Leader2000  你需要申请的2000我批准了

优化:

看到这里是不是很简单?虽然是简单,但是并不符合我们写代码的规范,所以我们先优化一下代码,优化代码的出发点如下:

  1. 具体是谁申请出差费用我们不管,我们只管申请出差需要做那些事!

  2. 具体是谁审批我们不需要纠结,我们只管审批需要做哪些事

针对以上两点,我们可以把申请人抽象成类,把审批人做成接口

申请人的抽象类如下:

/**
 * 将申请人的操作抽象化  不需要关心是谁来申请出差费用 只关心出差申要做的事
 */
public abstract class ApplyInfo {

    int money;

    public abstract void setMoney(int money);

    public abstract int getMoney();

    public void getApply() {
        System.out.print("老大我需要的出差费用是" + getMoney());
    }

}

审批人的接口如下:

/**
 * 将领导需要做的事抽象成接口
 */
public interface LeaderInfo {
    void handler(ApplyInfo person);
}

这样我们再对员工与审批人进行构建实体类

实体员工类:

/**
 * 申请人
 */
public class Person extends ApplyInfo {

    @Override
    public void setMoney(int money) {
        super.money = money;
    }

    @Override
    public int getMoney() {
        return super.money;
    }
}

实体审批人:

/**
 * 申请500已下的金额可以批,大于500会向上级回报
 */
public class Leader500 implements LeaderInfo {

    @Override
    public void handler(ApplyInfo person) {
        person.getApply();
        System.out.print("     我是Leader500  你需要申请的" + person.getMoney() + "我批准了");

    }
}

因为审批人都是实现相同的接口所以这里就不写完所有审批人的代码了,模版都是一样的。

这时员工做一次简单的申请会进行这样的操作:

 		//领导们
        Leader500 leader500 = new Leader500();
        Leader1000 leader1000 = new Leader1000();
        Leader1500 leader1500 = new Leader1500();
        Leader2000 leader2000 = new Leader2000();

        //申请人
        Person person = new Person();
        person.setMoney(2000);
        
        if (person.getMoney() <= 500) {
            leader500.handler(person);
        } else if (person.getMoney() <= 1000) {
            leader1000.handler(person);
        } else if (person.getMoney() <= 1500) {
            leader1500.handler(person);
        } else if (person.getMoney() <= 2000) {
            leader2000.handler(person);
        }

到这里,整体结构得到优化!

我们已经知道了需要申请多少钱的情况下直接用if-else if这样的模版进行申请的,这里根本就没有构成一个责任链中的链条模式,所以我们的代码还需要进行更多变化,于是我们在上边优化代码的结构下进行链式的构建:

申请人的抽象类基本没什么变化:

/**
 * 将申请人的操作抽象化  不需要关心是谁来申请出差费用 只关心出差申要做的事
 */
public abstract class ApplyInfo {
    int money;
    public abstract void setMoney(int money);
    public abstract int getMoney();
    public void getApply() {
        System.out.print("老大我需要的出差费用是" + getMoney());
    }
}

申请人这个时候不能作为接口,必须抽象成类:

/**
 * 将领导需要做的事抽象成类
 */
public abstract class LeaderInfo {

    //当前领导审核的金额
    int auditMoney;

    //当前领导的上级领导
    LeaderInfo superiorLeader;

    //设置当前领导能审批的额度

    public abstract void setCurrentMoney(int money);

    //当前领导收到申请后处理的事情
    public abstract void handler(ApplyInfo applyInfo);

    //设置当前领导的上一级领导
    public abstract void setSuperiorLeader(LeaderInfo superiorLeader);

    //执行审批流程
    public void dealInfo(ApplyInfo applyInfo) {
        if (applyInfo.money <= auditMoney) {
            handler(applyInfo);
        } else {
            superiorLeader.dealInfo(applyInfo);
        }
    }
}

注:

  1. 增加了审批人设置当前人可以审批的金额

  2. 设置当前审批人的直属上级领导

  3. 当前审批人收到申请后要做的事

  4. 执行审批流程

构建申请人的实体类:

/**
 * 申请人
 */
public class Person extends ApplyInfo {

    @Override
    public void setMoney(int money) {
        super.money = money;
    }

    @Override
    public int getMoney() {
        return super.money;
    }
}

领导:

/**
 * 申请500已下的金额可以批,大于500会向上级回报
 */
public class Leader500 extends LeaderInfo {


    @Override
    public void setCurrentMoney(int money) {
        super.auditMoney = money;
    }

    @Override
    public void handler(ApplyInfo applyInfo) {
        applyInfo.getApply();
        System.out.print("     我是Leader500  你需要申请的" + applyInfo.getMoney() + "我批准了");
    }

    @Override
    public void setSuperiorLeader(LeaderInfo superiorLeader) {
        super.superiorLeader = superiorLeader;
    }
}

。。。。。以此类推领导类

public static void main(String[] args) {

        //领导们
        Leader500 leader500 = new Leader500();
        Leader1000 leader1000 = new Leader1000();
        Leader1500 leader1500 = new Leader1500();
        Leader2000 leader2000 = new Leader2000();

        leader500.setCurrentMoney(500);
        leader1000.setCurrentMoney(1000);
        leader1500.setCurrentMoney(1500);
        leader2000.setCurrentMoney(2000);

        leader500.setSuperiorLeader(leader1000);
        leader1000.setSuperiorLeader(leader1500);
        leader1500.setSuperiorLeader(leader2000);

        //申请人
        Person person = new Person();
        person.setMoney(2000);

        leader500.dealInfo(person);

    }

执行代码中,显示构建了领导实例对象,然后每个领导能审批的金额是多少进行设置,再对领导人的上级领导进行关联,这样领导人就形成了一条链,我们再将申请人放入这条链,让他从链的开头进行滚动执行,直到有领导审批为止!

总结:

责任链模式就是使多个对象都有计划处理请求,从而避免请求的发送者和接受者之间的耦合关系。将 这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 Android中很多地方用到了这个设计模式,例如事件的传递,okhttp中的拦截器等等!

3.1.7 状态模式

简介:

状态模式(State Pattern)中,类的行为是基于它的状态改变的,状态之间的切换,在状态A执行完毕后自己控制状态指向状态B,状态模式是不停的切换状态执行,这种类型的设计模式属于行为型模式。

状态模式角色

State: 抽象状态类,定义一个接口以封装与context的一个状态相关的行为

ConcreteState: 具体状态,每一子类实现一个与Context的一个状态相关的行为

Context: 状态上下文,维护一个ConcreteState子类的实例,这个实例定义当前的状态。

状态模式抽象类方法类型

上下文抽象方法:request,上下文处理请求。

状态抽象方法:handle,状态行为方法,不同的状态,行为不同。

状态模式与策略模式的区别:

状态是系统自身的固有的,调用者不能控制系统的状态转移。比如,一个请假单有“部长审批”-“经理审批”-“审批通过”-“审批不通过”等状态,请假者没有办法将一个部长都还没审批完的请假单提交给经理,这个状态转换只能系统自己完成。

策略是外界给的,策略怎么变,是调用者考虑的事情,系统只是根据所给的策略做事情。

环境角色的职责不同

两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。

解决问题的重点不同

策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。

解决问题的方法不同

策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。

复杂度不同

通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易阅读。状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正

示例:

假设现在我们有一个饮水机,它有以下两个状态: 满桶,空桶。初始状态是满桶,容量是20。饮水机只有一个动作:press,每次press后都会使容量减1,一旦为0,则将状态设置为空桶,这时press没有水流出。

要使用状态模式,我们必须明确两个东西:状态和每个状态下执行的动作。就像是饮水机,最基本的状态就是满桶和空桶,而这两个状态下,都可能要执行倒水这个动作,也就是press。如果饮水机的容量为0,则会进入空桶的状态。

在状态模式中,因为所有的状态都要执行相应的动作,所以我们可以考虑将状态抽象出来。

状态的抽象一般有两种形式:接口和抽象类。如果所有的状态都有共同的数据域,可以使用抽象类,但如果只是单纯的执行动作,就可以使用接口。

public interface DispenserState {
    void press();
}

然后我们再定义满桶和空桶两个状态:

public class FullState implements DispenserState {

    @Override
    public void press() {
        System.out.println("Water is pouring!");
    }
}

public class NullState implements DispenserState {

    @Override
    public void press() {
        System.out.println("There is not water poured!");
    }
}

接着我们再实现饮水机:

public class WaterDispenser {
    private static int capacity = 20;
    private static DispenserState dispenserState;

    public WaterDispenser(DispenserState state) {
        dispenserState = state;
    }

    private static void setState(DispenserState state) {
        dispenserState = state;
    }

    public DispenserState getState() {
        return dispenserState;
    }

    public void press() {
        capacity--;
        if (capacity <= 0) {
            setState(new NullState());
        }
        dispenserState.press();
    }
}

接着我们再进行测试:

public class Test {
    public static void main(String[] args) {
        WaterDispenser dispenser = new WaterDispenser(new FullState());
        for (int i = 0; i < 21; ++i) {
            dispenser.press();
        }
    }
}

输出:

Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
Water is pouring!
There is not water poured!
There is not water poured!

我们不断的press,饮水机里的水会越来越少,从满桶状态变成空桶状态。

3.2 算法
3.2.1 数组

遍历

public static void main(String[] args){
    String[] arr = new  String[]{"xx","yy","zz"};
    for (int i=0;i<arr.length;i++){
        System.out.println(arr[i]);
    }

    System.out.println();

    for (String elm : arr){
        System.out.println(elm);
    }
}

查找:

普通查找:

public class BaseSearch {

    private static boolean searchMode01(int[] arr, int mum) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i]==mum) {
                //在数组中
                return true;
            }
        }
        //不在数组中
        return false;
    }
    
    public static void main(String[] args) {
        int[] arr = new int[] {1,2,3,4,5,6,7,8};
        System.out.println(searchMode01(arr, 10));
        //结果false
    }

}

二分查找:

步骤: 1、定义最小索引和最大索引 2、计算中间索引 3、拿中间索引对应的数值和需要查找的数进行比较 数值= 查找的数 返回中间索引 数值 > 查找的数 在左边找 数值 查找的数 在右边找 4、重复第二步 5、如果最小的数的值比最大值还要大,那么说明没有找到返回-1 6、二分查找的数组或者集合必须是有序的**

public class binarySearch {
    /**
     * @Title: main
     * @Description:二分查找数组中的元素
     * @param
     * @return void 返回类型
     * @throws
     */
    public static int findArrValue(int[] arr,int mum) {
//定义数组最小元素索引
        int min=0;
//定义数组最大元素索引
        int max=arr.length-1;
//定义数组中间元素索引
        int mid = (min+max)/2;
//判断中间值是否等于输入的数
        while (arr[mid]!=mum) {
//判断中间索引值是否小于mum
            if (arr[mid]<mum) {
//mum比中间值大,在右边,所以最小索引min需要中间值mid+1
                min=mid+1;
            }else if(arr[mid]>mum) {
//mum比中间值小,在左边,所以最大索引值max需要中间索引值mid+1
                max=mid-1;
            }
//如果一直递增的最小索引大于一直递减的最大缩影,那么就是没有找到
            if (min>max) {
                return -1;
            }
//每次计算完之后,min和max都发生改变,中间索引值需要重新计算
            mid = (min+max)/2;

        }
        return mid;

    }

    public static void main(String[] args) {
        int[] arr = new int[] {11,22,33,44,55,66,77,88};
//返回0,表示在数组中,-1表示不再数组中
        System.out.println(findArrValue(arr, 11));
//结果 0 在数组中
    }

}

排序:

package com.example.myapplication.arraytest;

public class ArrayOption {
    /**
     * 冒泡排序,两两进行比较
     * @param array
     * @param n
     */
    void BubbleSort(int array[], int n)
    {
        int i, j, k;
        for(i=0; i<n-1; i++)
            for(j=0; j<n-1-i; j++)
            {
                if(array[j]>array[j+1])
                {
                    k=array[j];
                    array[j]=array[j+1];
                    array[j+1]=k;
                }
            }
    }

    /**
     * 选择排序,将最小的元素移到最前面
     * @param arr
     */
    void CheckSort(int[] arr){
        for(int i=0;i<arr.length;i++) {
            int tem=i;
            //将数组中从i开始的最小的元素所在位置的索引赋值给tem
            for(int j=i;j<arr.length;j++) {
                if(arr[j]<arr[tem]) {
                    tem=j;
                }
            }
            //上面获取了数组中从i开始的最小值的位置索引为tem,利用该索引将第i位上的元素与其进行交换
            int temp1=arr[i];
            arr[i]=arr[tem];
            arr[tem]=temp1;
        }
    }

    /**
     * 将数组倒序
     * @param arr
     */
    void ReversalSort(int[] arr){
        for(int i=0;i<arr.length/2;i++) {
            int tp=arr[i];
            arr[i]=arr[arr.length-i-1];
            arr[arr.length-i-1]=tp;
        }
    }

    /**
     * 插入排序,将大的放到后面
     * @param arr
     */
    void InsertSort(int[] arr){
        for(int i=1;i<arr.length;i++){
            for (int j=i;j<arr.length;j++){
                if (arr[j-1]>arr[j]){
                    int tem = arr[j-1];
                    arr[j-1] = arr[j];
                    arr[j] = tem;
                }
            }
        }
    }

}

插入:

    /**
     * 将数字插入数组的任意位置
     * @param arr
     * @param position
     * @param value
     * @return
     */
    public static int[] arrayAnyInsert(int[] arr,int position,int value){
        int[] res=new int[arr.length+1];
        if(position<0 || position>res.length){
            return null;
        }
        for(int i=0;i<res.length;i++){//将原数组排在position之前的元素复制到res中
            if(i<position){             //在position上插入元素,position后面的元素依次向后移动一位
                res[i]=arr[i];
            }else if(i==position){
                res[i]=value;
            }else{
                res[i]=arr[i-1];
            }
        }
        return res;
    }

删除:

/**
 * 删除单个元素
 * 删除指定位置上的元素
 * @param arr
 * @param position
 * @return
 */
public static int[] delAnyPosition(int[] arr,int position){
    //判断是否合法
    if(position >= arr.length || position < 0){
        return null;
    }
    int[] res = new int[arr.length - 1];
    for(int i = 0;i<res.length;i++){//将arr复制到res上的时候,略过position上的元素
        if(i < position){
            res[i] = arr[i];
        }else{
            res[i] = arr[i + 1];
        }
    }
    return res;
}
/**
 * 去除数组中重复的元素
 * @param str
 * @return
 */
public static String[] removere(String[] str){
    for (String elementA:str ) {
        System.out.print(elementA + " ");
    }
    List<String> list = new ArrayList<String>();
    for (int i=0; i<str.length; i++) {
        if(!list.contains(str[i])) {
            list.add(str[i]);
        }
    }
    String[] newStr =  list.toArray(new String[1]); //返回一个包含所有对象的指定类型的数组
    for (String elementB:newStr ) {
        System.out.print(elementB + " ");
    }
    return newStr;
}
3.2.2 链表

创建Node 节点类:

package com.example.myapplication.listtest;

public class Node {
    private String data;        //节点保存的数据
    private Node next;            //下个节点

    public Node(String data){
        this.data = data;
    }

    public String getData()
    {
        return data;
    }
    public void setData(String data)
    {
        this.data = data;
    }

    public Node getNext()
    {
        return next;
    }
    public void setNext(String data)
    {
        this.next = new Node(data);
    }

    /*Description: 添加节点
     *return     :
     */
    public void addNode(String data)
    {
        if(getNext()!=null)
        {
            this.next.addNode(data);
        }
        else
        {
            this.setNext(data);
        }
    }

    /*Description: 获取节点数据
     *return     :
     */
    public String getData(int index)
    {
        String ret =null ;
        if(index == 0)    //如果递归到0,则返回当前数据
        {
            ret=data;
        }
        else            //否则继续递归查找
        {
            ret=this.next.getData(--index);
        }
        return ret;
    }

    /*Description:  递归地查找data位于链表哪个序号
     *return     :  -1(表示未找到)
     */
    public int findIndex(String data,int index)
    {
        if(this.data.equals(data))    //已找到
        {
            return index;
        }
        else if (getNext()==null)    //未找到
        {
            return -1;
        }

        return this.next.findIndex(data,++index);

    }

    /**
     *Description:  递归地查找data,并删除
     *data: 要找的data
     *PreNode:    上个节点,如果为null则当前位于表头
     *index: 表示当前位于链表哪个序号
     *return  :  -1(表示未找到) 0~(len-1) (表示data位于链表哪个序号)
     */
    public int delData(String data,Node PreNode,int index)
    {
        int ret = -1;

        if(this.data.equals(data))    //删除
        {
            PreNode.next = this.next;
            return index;
        }
        else if (getNext()==null)    //未找到
        {
            return ret;
        }

        return this.next.delData(data,this,++index);
    }
}

创建链表类:

package com.example.myapplication.listtest;

public class LinkList {
    private Node next;        //负责管理的节点
    private int len;        //统计节点长度

    public LinkList(String data)
    {
        next = new Node(data);
        len =1;
    }

    /*Description:  添加一个节点数据
     *return     :
     */
    public void addData(String data)
    {
        this.next.addNode(data);
        len++;
    }


    /*Description:  删除一个节点数据
     *return     :  -1(未找到要删除的数据) 0~(len-1) (表示data位于链表哪个序号)
     */
    public int delData(String data)
    {
        int ret=-1;
        if(len>=0)                //链表有数据
        {
            if(this.next.getData().equals(data))    //删除表头需要特殊处理
            {
                this.next = this.next.getNext();
                ret = 0;
            }
            else
                ret = next.delData(data,this.next,1);
        }

        if(ret!= -1)    //已删除
        {
            len--;
        }

        return ret;
    }


    /*Description:  根据index找到对应的节点数据
     *return     :  返回节点数据
     */
    public String getNodeData(int index)
    {
        String ret=null;
        if(index>=0 && index<(len))
        {
            ret = next.getData(index);
        }
        return ret;
    }

    /*Description:  根据data查找节点Node位于链表哪个序号
     *return     :  -1(表示未找到)  0~(len-1) (表示data位于链表哪个序号)
     */
    public int findNodeIndex(String data)
    {
        int ret=-1;
        if(len>=0)                //链表有数据
        {
            ret = next.findIndex(data,0);  //从序号0开始找
        }
        return ret;
    }

    /*Description:  将链表中所有的节点数据转为数组
     *return     :
     */
    public String[] toArrays()
    {
        Node tmp=this.next;

        String[] arr = new String[len];


        for(int i=0; i< len; i++)
        {
            arr[i] = tmp.getData();
            tmp = tmp.getNext();
        }
        return arr;
    }

    public int length()
    {
        return len;
    }
}

测试:

package com.example.myapplication.listtest;

public class Test {
    public static void main(String[] args){
        LinkList list = new LinkList("小A");

        //添加节点数据
        list.addData("小B");
        list.addData("小C");
        list.addData("小D");
        list.addData("小E");


        //打印节点数据
        System.out.println("print Node data:");
        for(int i=0;i<list.length();i++)
        {
            System.out.println(list.getNodeData(i));
        }
        System.out.println("---------------------");

        //查找节点数据的序号
        System.out.println("小A的index位于:"+list.findNodeIndex("小A"));
        System.out.println("小D的index位于:"+list.findNodeIndex("小D"));
        System.out.println("小F的index位于:"+list.findNodeIndex("小F"));  //返回-1,表示未找到

        //删除节点数据
        System.out.println("删除小A,并打印小A之前的位置:"+list.delData("小A"));
        System.out.println("删除小E,并打印小E之前的位置:"+list.delData("小E"));
        //通过数组打印数据
        System.out.println("\r\nprint Node data by toArrays() :");
        String[] arr=list.toArrays();
        for(int i=0;i<arr.length;i++)
        {
            System.out.println(arr[i]);
        }
        System.out.println("---------------------");
    }
}

输出:

小A
小B
小C
小D
小E
---------------------
小A的index位于:0
小D的index位于:3
小F的index位于:-1
删除小A,并打印小A之前的位置:0
删除小E,并打印小E之前的位置:4

print Node data by toArrays() :
小B
小C
小D
---------------------

原文地址:https://www.cnblogs.com/littleboy123/p/12979055.html