Java教程

 

抽象类

抽象类定义

Java语言提供了两种类:具体类和抽象类
在Java中,abstract是抽象的意思,可以修饰类,成员方法
abstract修饰类,就是抽象类,abstract修饰方法,就是抽象方法
格式如下:

修饰符 abstract class 类名{
修饰符 abstract 返回值类型 方法名称(形参列表)
}
注意:一个类中有抽象方法的话,该类也要被abstract修饰,否则会报错
示例:

//abstract修饰的抽象类
public abstract class People {
 
    //abstract修饰的抽象方法
    public abstract void eat();
}

特点

  • 修饰符必须为public或者protected,不能为private,因为创建了抽象类,就要被其>他类继承,设为private就无法被继承
  • 抽象类不能被实例化,只能通过普通子类来实现实例化
    如果一个普通子类继承于抽象父类,则该类一定要重写实现该父类的抽象方法,如果»- 子类仍然的hi一个抽象类,也是允许的,就不必冲写父类的抽象方法,但必须用abstract修饰
  • 抽象级别:抽象类是对整体类的抽象,包括属性和方法
  • 抽象类是现有子类,将子类的共同属性和方法提取出来放在抽象类中,是一种从下往上的构建法则
  • 父类包含了子类集合的常见方法,但由于父类本身是抽象的,不能使用这些方法

抽象方法

抽象类方法特点

  • 抽象方法中没有方法体
  • 抽象方法必须存在于抽象类中
  • 子类重写父类时,必须重写父类的所有抽象方法
  • 抽象方法不能使用private final static 来修饰,因为这些关键字都是和重写违背的

抽象方法代码例子:

//abstract修饰的抽象类
public abstract class People {
 
 
    //普通方法,带 {} 方法体
    public void fun(){
        System.out.println("存在方法体的普通方法");
    }
 
 
    //abstract修饰的抽象方法,没有方法体 {},被abstract修饰
    public abstract void eat();
}

抽象类举例

例子1
public class Cat {
 
    public void sleep(){
        System.out.println("我在睡觉");
    }
}

public class Dog{
 
    public void sleep(){
        System.out.println("我在睡觉");
    }
}

现在需要增加一个eat的行为,各动物吃的不一样

//定义一个abstract修饰的抽象类
public abstract class Animal {
 
    //通用的行为sleep也放在父类中
    public void sleep(){
        System.out.println("我在睡觉");
    }
    
    //定义抽象方法
    //抽象方法没有方法体{}
    public abstract void eat();
}


public class Cat extends Animal{
 
    //@Override注解是重写父类的方法
   @Override
    public void eat(){
       System.out.println("我在吃鱼");
   }
}

public class Dog extends Animal{
 
    //@Override注解是重写父类方法需要引入的注解
    @Override
    public void eat(){
        System.out.println("我在吃骨头");
    }
 
}

接口

接口定义

英文称作_interface_,在软件工程中,接口泛指供别人调用的方法或者函数。从这里,我们可以体会到Java语言设计者的初衷,它是对行为的抽象。

[public] interface InterfaceName {
  void method1();
  void method2();
}
  • 接口中可以含有变量和方法
  • 变量会被隐式地指定为_public static final_变量,并且只能是public static final变量,用private修饰会报编译错误
  • 方法会被隐式地指定为_public abstract_方法且只能是public abstract方法,用其他关键字,比如private、protected、static、 final等修饰会报编译错误
  • 并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

接口实现

Class MyClass \[extends Class 父类\] Implements 接口1接口2,…接口N{

// @Override 接口方法

}

接口举例

继续上文的例子使用接口重构

//定义一个abstract修饰的抽象类
public abstract class Animal {
 
    //通用的行为sleep也放在父类中
    public void sleep(){
        System.out.println("我在睡觉");
    }
}
// 定义一个吃的接口
public interface IEat{
    void eat();
}


public class Cat extends Animal implements IEat{
 
    //@Override注解是重写父类的方法
   @Override
    public void eat(){
       System.out.println("我在吃鱼");
   }
}

public class Dog extends Animal  implements IEat{
 
    //@Override注解是重写父类方法需要引入的注解
    @Override
    public void eat(){
        System.out.println("我在吃骨头");
    }
 
}

抽象类和接口区别

1.语法层面上的区别

  1)抽象类可以提供成员方法的实现细节,而接口中只能存在_public abstract 方法_;

  2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;

  3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。

2.设计层面上的区别

  1)抽象类是对一种_事物的抽象,即对类抽象,而接口是对行为_的抽象。

  抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

  举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将_飞行这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将飞行_设计为一个接口Fly,包含方法fly(),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个”是不是”的关系,而接口实现则是 “有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

  2)设计层面不同,抽象类作为很多子类的父类,它是一种_模板式设计。而接口是一种行为规范,它是一种辐射式_设计。

  什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。

  而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

     下面看一个网上流传最广泛的例子:

  门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:

abstract class Door {
    public abstract void open();
    public abstract void close();
}
  或者
interface Door {
    public abstract void open();
    public abstract void close();
}

  但是现在如果我们需要门具有报警alarm()的功能,那么该如何实现?下面提供两种思路:

  1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;

  2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open()和close(),也许这个类根本就不具备open()和close()这两个功能,比如火灾报警器。

  从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。

interface Alram {
    void alarm();
}
 
abstract class Door {
    void open();
    void close();
}
 
class AlarmDoor extends Door implements Alarm {
    void oepn() {
      //....
    }
    void close() {
      //....
    }
    void alarm() {
      //....
    }
}

须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法

拆箱装箱基本概念

  • 什么是装箱和拆箱
      Java中的装箱(boxing)和拆箱(unboxing)是指将基本数据类型与其对应的包装类之间进行转换的过程。

  • 装箱
      装箱是将基本数据类型转换为其对应的包装类,例如将int类型转换为Integer类型。这个过程是通过自动装箱(autoboxing)或手动装箱(manual boxing)完成的。自动装箱是指Java编译器自动将基本类型转换为包装类型,而手动装箱则是通过调用包装类的构造函数来完成。

  • 拆箱
      拆箱是将包装类转换为其对应的基本数据类型,例如将Integer类型转换为int类型。这个过程也是通过自动拆箱(autounboxing)或手动拆箱(manual unboxing)完成的。自动拆箱是指Java编译器自动将包装类型转换为基本类型,而手动拆箱则是通过调用包装类的xxxValue()方法来完成。

包装类型的必要性

  • 基本类型包装列表

error

  • 对象引用:基本类型不是对象,无法使用对象引用,而包装类型是对象,可以通过引用来操作。
  • 泛型:泛型不能使用基本数据类型,必须使用包装类型。如,使用List<Integer>来存储一组整数,而不是使用List<int>
  • null值:基本类型不能为null,而包装类型可以为null。
  • 类型转换:包装类型提供了许多类型转换方法,例如将字符串转换为整数或浮点数等,这些方法非常方便。
  • 方法重载:方法重载可以使用不同的参数类型,但是基本数据类型和其对应的包装类型在方法中被认为是相同的类型,因此可以在方法中使用它们进行重载。

装箱和拆箱举例

从Java 5开始,引入了自动装箱和拆箱机制。这意味着我们可以直接将基本数据类型赋值给对应的封装类型变量,或将封装类型变量赋值给对应的基本数据类型变量,编译器会自动完成转换。例如:

int x = 10;
Integer y = x;  // 自动装箱
int z = y;  // 自动拆箱
int x = y.intValue();  // JDK1.5 之前 手动拆箱

缓存池问题

Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

包装类常用方法

error

error

error

error

error

error

error

error

error

String 和 StringBuilder、StringBuffer

String

String 被声明为 final,因此它不可被继承。在 Java 8 中,String 内部使用 char 数组存储数据。value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

**不可变的好处:**

  • 1.可以缓存 hash 值
    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  • 2.String Pool 的需要
    如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

error

  • 3.安全性
    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

  • 4.线程安全
    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

**字符串的两种比较方式:==;equals()**
==比较的是;两个String对象地址空间是否相等,判断的是地址值;
equals()判断String类型的内容是否相等。开发中 我们一般用的是equals()方法

StringBuilder 和 StringBuffer

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

  • StringBuffer

StringBuffer 对象 代表一个可变的字符串序列,当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其 toString 方法来生成一个新的字符串。例如

StringBuffer testString= new StringBuffer("111");
testString.append("222");
System.out.println(testString);

append方法增加synchronized关键字保证了线程安全

error

  • StringBuilder

StringBuilder 其实是和 StringBuffer 几乎一样,只不过 StringBuilder 是非线程安全的

error

使用场景:

错误方式:

String builder = new String("aaa");
for (int i = 0; i < 10000; i++) {
    builder+="bbb"; // 每次循环都new StringBuilder().append()
}
builder.toString();

正确方式:

String builder = new String("aaa");
for (int i = 0; i < 10000; i++) {
    builder+="bbb"; // 每次循环都new StringBuilder().append()
}
builder.toString();

(一) 匿名类

概念

匿名类指的是没有类名的类,通常是通过继承或实现一个接口来实现的。匿名类的主要作用是简化代码,使代码更加简洁,易于阅读和编写。但是由于其特殊性质,匿名类的使用也较为复杂,需要仔细理解和掌握。

匿名类最早出现在Java的内部类中,但是自从Java 8中引入了Lambda表达式之后,匿名类也几乎被广泛应用于函数式编程中

语法

匿名类的语法比较简单,通常通过继承或实现一个接口来创建。下面是匿名类的基本语法:

new SuperClass() {
 // 匿名类的成员定义
}
//或者
new Interface() {
 // 匿名类的成员定义
}

应用举例

-排序和过滤
在Java的集合框架中,可以使用匿名类来实现自定义的排序和过滤器。下面是一个使用匿名类对数组进行排序的例子:

Arrays.sort(array, new Comparator() {
 public int compare(String s1, String s2) {
 return s1.compareTo(s2);
 }
});

-lambda 表达
自从Java 8中引入了Lambda表达式之后,匿名类在函数式编程中的应用越来越广泛。Lambda表达式可以替代匿名类,并且其语法更加简洁明了。下面是一个使用Lambda表达式实现排序的例子:

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

匿名类的优缺点

  • 优点
    匿名类最大的优点是可以简化代码,使代码更加简洁,易于阅读和编写。此外,匿名类还可以在其定义的作用域之外访问外部变量和方法。

  • 缺点
    匿名类的缺点是在调试和维护时比较困难,因为无法直接查看匿名类的代码。而且匿名类无法重载,也无法定义构造方法,无法进行复杂的初始化操作

(二)内部类

内部类概念

在一个类的内部定义一个类或者方法内,这样类成为内部类,内部类可以有效的控制类的访问控制以及方便内部类对宿主类成员和方法访问,编译后生成的class会体现宿主类名比如 AClass$InnerClass.class。
内部类可分为:静态内部类、实例内部类、匿名内部类、局部内部类。其中的局部内部类是在方法里面定义的类,几乎不用。

内部类语法

//这是一个外部类
class OutClass {
    //这是一个普通内部类
    class NeiBuClass {
        
    }
}

例子

  • 普通内部类
class OutClass {
    public int num1;
    private int num2;
    protected int num3;
    public void fun() {
        System.out.println("今天过得很开心!");
    }
    class NeiBuClass {
        public void show() {
            //外部类里的任何属性都内被内部类使用
            num1 = 1;
            num2 = 2;
            num3 = 3;
            System.out.println(num1+" "+num2+" "+num3);
            //访问外部类成员方法
            fun();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        //实例化内部类前得先实例化外部类
        OutClass.NeiBuClass neiBuClass = new OutClass().new NeiBuClass();
        neiBuClass.show();
    }
}
  • 静态内部类
//外部类
class OutClass {
    //外部类成员
    public int num1 = 10;
    private int num2 = 20;
    protected int num3 = 30;
    public static int num4 = 40;
    //静态内部类
    static class NeiBuClass {
        public void show() {
            //实例化一个外部类对象  直接访问将会出错【注意区分普通内部类】
            OutClass outClass = new OutClass();
            //通过外部类对象来引用外部类属性
            System.out.println(outClass.num1);
            System.out.println(outClass.num2);
            System.out.println(outClass.num3);
            System.out.println(num4);
        }
    }
}
 
public class Test {
    public static void main(String[] args) {
        //创造一个静态内部类对象
        OutClass.NeiBuClass neiBuClass = new OutClass.NeiBuClass();
        neiBuClass.show();
    }
}

  • 匿名内部类
//定义一个名为IUSB的接口
interface IUSB {
    void show();
}
//Try类使用了这个接口
class Try implements IUSB{
    //重写了接口中的show方法
    @Override
    public void show() {
        System.out.println("今天你开心吗?");
    }
}
 
public class Test {
    public static void main(String[] args) {
        //new一个接口
        new IUSB() {
            @Override
            public void show() {
                System.out.println("这就是一个匿名内部类");
            }
        }.show();
    }
}
  • 局部内部类
public class Test {
    //一个名为show的方法
    public void show() {
        //这是一个局部内部类
        class Dog {
            //局部内部类的成员变量
            public int num = 10;
            //局部内部类中的成员方法
            public void fun() {
                System.out.println("这是局部内部类中的fun()方法");
            }
        }
        //实例化一个局部内部类对象
        Dog dog = new Dog();
        //通过dog引用来方法这两个属性
        System.out.println(dog.num);
        dog.fun();
    }
    public static void main(String[] args) {
        //再通过实例化一个主类才能调用show方法
        Test test = new Test();
        test.show();
    }
}

设计模式与UML

(一) 工厂方法模式(Factory Method)

工厂方法模式分为三种:

1、普通工厂模式

  普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

error

  举例如下:(我们举一个发送邮件和短信的例子)
  首先,创建二者的共同接口:

public interface Sender {  
    public void Send();  
}  

  其次,创建实现类:

public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}  
public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}  

  最后,建工厂类:

public class SendFactory {  
  
    public Sender produce(String type) {  
        if ("mail".equals(type)) {  
            return new MailSender();  
        } else if ("sms".equals(type)) {  
            return new SmsSender();  
        } else {  
            System.out.println("请输入正确的类型!");  
            return null;  
        }  
    }  
}  

  我们来测试下:

public class FactoryTest {  
  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produce("sms");  
        sender.Send();  
    }  
}  

  输出:

this is sms sender!

2、多个工厂方法模式

  多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:

error

  将上面的代码做下修改,改动下SendFactory类就行,如下:

public   class  SendFactory {  
    public  Sender produceMail(){  
        return new MailSender();  
    }  
      
    public Sender produceSms(){  
        return new SmsSender();  
    }  
}  

  测试类如下:

public class FactoryTest {  
  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produceMail();  
        sender.Send();  
    }  
}  

  输出:

this is mailsender!

3、静态工厂方法模式

  静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

public class SendFactory {  
      
    public static Sender produceMail(){  
        return new MailSender();  
    }  
      
    public static Sender produceSms(){  
        return new SmsSender();  
    }  
}  
public class FactoryTest {  
  
    public static void main(String[] args) {      
        Sender sender = SendFactory.produceMail();  
        sender.Send();  
    }  
}  

  输出:

this is mailsender!

  总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式

4、抽象工厂模式(Abstract Factory)

工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。

error

  请看例子:

public interface Sender {  
    public void Send();  
}  
&emsp;&emsp;两个实现类
public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}  
public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}  

  两个工厂类:

public class SendMailFactory implements Provider {  
      
    @Override  
    public Sender produce(){  
        return new MailSender();  
    }  
}  
public class SendSmsFactory implements Provider{  
  
    @Override  
    public Sender produce() {  
        return new SmsSender();  
    }  
}  

  在提供一个接口:

public interface Provider {  
    public Sender produce();  
}  

  测试类:

public class Test {  
  
    public static void main(String[] args) {  
        Provider provider = new SendMailFactory();  
        Sender sender = provider.produce();  
        sender.Send();  
    }  
}  

(二) 单例模式(Single)

基本概念

单例模式是一种设计模式,它确保一个类只能创建一个实例,并提供一种全局访问这个实例的方式。在Java中,单例模式可以通过多种方式来实现,其中最常见的是使用私有构造函数和静态方法实现

模式实现方式

  • 懒汉式单例模式
    懒汉式单例模式指的是在第一次使用单例对象时才创建实例。具体实现方式是在getInstance()方法中判断实例是否已经被创建,如果没有则创建一个新实例并返回。懒汉式单例模式的缺点是线程不安全,在多线程环境下可能会创建多个实例。
public class Singleton {
private static Singleton instance;

private Singleton() {
// 私有构造函数
}

public static Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
}

}
  • 饿汉式单例模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 私有构造函数
}

public static Singleton getInstance() {
    return instance;
}
}
  • 双重检验锁单例模式
public class Singleton {
private static volatile Singleton instance;

private Singleton() {
// 私有构造函数
}

public static Singleton getInstance() {
  if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
        instance = new Singleton();
    }
  }
}
   return instance;
}

  • ThreadLocal单例模式
public class Singleton {
private static final ThreadLocal<Singleton> INSTANCE = new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
    return new Singleton();
}
};

private Singleton() {
// 私有构造函数
}

public static Singleton getInstance() {
    return INSTANCE.get();
}
}
  • 注册式单例模式
    注册式单例模式指的是通过一个注册表来管理所有单例对象,从而实现单例模式。具体实现方式是在一个静态的Map中保存所有单例对象,然后在需要使用单例对象时通过Map来获取。
    public class Singleton {  
    private static Map<String, Singleton> instances = new HashMap<>();  
    private Singleton() {  
    // 私有构造函数  
    }

    public static Singleton getInstance(String name) {  
    if (!instances.containsKey(name)) {  
    instances.put(name, new Singleton());  
    }  
    return instances.get(name);  
    }  
    }

(三) 模板方法模式

模板方式基本概念

模板方法模式定义了一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤
比如我们大家平时看病的时候,肯定会先挂号→排队等叫号→看病→付款→拿药。其中这个过程中,其实只有看病的过程是取决于我们得了什么病,是不确定的,而其他过程都是固定的流程,因此就可以用模板方法模式。

代码实现

public abstract class AbstractPatient {//抽象类
 
    //通常我们为了防止恶意修改,一般来说模板定下来就不变了,因此模板方法用final修饰
    public final void treatmentProcedure(){ //就诊流程(模板方法)
        //将我们的普通方法组合起来
        Registration();
        WaitInLine();
        Treatment();
        Pay();
        GetMedicine();
    }
 
    private void registration(){ //挂号
        System.out.println("挂号......");
    }
    private void waitInLine(){ //等待叫号
        System.out.println("等待叫号......");
    }

    protected abstract void treatment(); //看病
    
    private void pay(){ //付款
        System.out.println("付款......");
    }
    private void getMedicine(){ //取药
        System.out.println("取药......");
    }
}

public class PatientOne extends AbstractPatient{ //病人1
    @Override
    public void treatment() { //病人1的看病流程
        System.out.println("发烧了,先挂瓶然后吃退烧药");
    }
}


public class PatientTwo extends AbstractPatient { //病人2
    @Override
    public void treatment() {//病人2的看病流程
        System.out.println("风寒了,拔罐、针灸......");
    }
}

public class Test {
    public static void main(String[] args) {
        PatientOne patientOne = new PatientOne();
        PatientTwo patientTwo = new PatientTwo();
        System.out.println("以下是病人①的看病流程:");
        patientOne.TreatmentProcedure();
        System.out.println("----------分界线----------");
        System.out.println("以下是病人②的看病流程:");
        patientTwo.TreatmentProcedure();
    }
}

(四)认识类图

类图基本概念

类图(class diagram)使用出现在系统中的不同类来描述系统的静态结构,它用来描述不同的类以及它们之间的关系。

在系统分析和设计阶段,类通常可以分为三种,分别是实体类(Entity class)、控制类(Control Class)和边界类(Boundary Class)。

1)实体类:对应系统中的每个实体,它们通常需要保存在永久存储体中,一般使用数据库或文件表来记录,实体类既包括存储和传递数据的类,还包括操作数据的类。实体类来源于需求说明中的名词,如学生、商品等。

2)控制类:用于体现应用程序的执行逻辑,提供相应的业务操作,将控制 类抽象出来可以降低界面和数据库之间的耦合度。控制类一般是由动宾结构的短语(动词加名词)转化来的名词,比如增加商品类,用户注册类。

3)边界类:用于对外部用户和系统之间的交互对象进行抽象,主要包括界面类,如对话框、窗口、菜单。

类图的UML 表示

关于可见性: + public,- private,# protected

error

类的关系

泛化关系

  • 类的继承

error

  • 接口的实现

error

关联关系

  • 聚合

比如汽车发动机是汽车的一部分,但是发动机也可以独立存在。

error

  • 组合
    比如,头是整体,嘴巴只是一个部分。嘴巴不能独立存在

error

  • 自关联

error

  • 多重性关联
    表示两个关联对象在数量上的对应关系。
    在UML中对象之间的多重性可以直接在关联直线上用一个数字或一个数字范围表示。
    常见的多重性表示如下图:
    error

比如一个界面(Form)可以拥有零个或多个按钮,一个按钮只能属于一个界面。

error

依赖关系

error

关键字

final

final概念

final 可用于修饰 类,方法,变量,使用final具备以下好处:

  • final方法比非final快一些
  • final关键字提高了性能。JVM和Java应用都会缓存final变量。
  • final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  • 使用final关键字,JVM会对方法、变量及类进行优化

规则说明

修饰类

final修饰类表示这个类是最终类,是不可被继承的。abstract与final是不能修饰同一个类的,因为当一个类被声明为abstract后就是要被用来继承,与final搭配是互相矛盾的。常见的final类有System,String…
String之所以被设计成final,或者说一个类之所以被设计为final,本意都是从安全性层面考虑的,用final就是拒绝继承,防止被重写破坏。

修饰方法

方法就不能被重写,但不影响子类调用父类中的fianl方法,也不影响重载。fianl与abstract不能同时修饰一个方法,因为abstract修饰的方法目的就是为了被子类去重写实现的,而fianl的作用是不允许该方法被子类重写,这是互相矛盾的

修饰变量

如果是基本数据类型(int,double,char)变量那么表示这个变量是不可变的,即表示一个常量,通常是大写,例如

public final static int NO_OPTIONS = 0;
public final static int ENCODE = 1;

如果是引用类型(String,Date,Animal,Person)的变量那么表示这个引用只能指向初始化时指向的那个对象,不能再指向别的对象,但被指向的这个对象的属性值并非不能修改.

final例子

  • final修改类

该类不能被继承,即不能有子类。这种情况下,该类的实现被认为是完整、安全和稳定的。

public final class MyClass {
    // 类实现
}
  • final 修改方法

当final修饰一个方法时,该方法不能被子类重写。这种情况下,该方法的实现被认为是最终的、安全的或固定的。例如,Object类中的getClass()方法就是一个final方法,它返回该对象的运行时类型信息,不能被子类修改。

public class ParentClass {
    public final void myMethod() {
        // 方法实现
    }
}
 
public class ChildClass extends ParentClass {
    // 这里编译错误,无法重写myMethod()方法
    public void myMethod() {
        // 子类实现
    }
}

  • final 修改变量
@Data
@AllArgsConstructor
class User {
    private int id;
    private String name;
    private int age;

    public User() {
    }
}


public class Test {

    @Test
    public void test(){
        final User user = new User(1001, "abc", 21);
        //User(id=1001, name=abc, age=21)
        System.out.println(user);
        user.setAge(22);
        //User(id=1001, name=abc, age=22)
        System.out.println(user);

    user=new User(1002,"abc",22); //  错误不可以改变应用
    }
}

synchronized

synchronized概念

synchronized关键字是Java中非常重要和常用的关键字,它主要是用来实现对象的同步和线程的互斥。使用synchronized关键字可以保证同一时间只有一个线程能够访问共享资源,其他线程必须等待当前线程执行完毕后才能继续访问。
在java 6 版本之后该锁进行了性能优化,因此也在众多框架中得到广泛应用。synchronized 修饰代码块,方法,静态方法,类。

synchronized例子

1)修饰代码块:使用synchronized关键字修饰的代码块,只能被一个线程访问。示例如下:

synchronized (obj) {  
// 被锁定的代码块  
}

2)修饰方法:使用synchronized关键字修饰的方法,同样只能被一个线程访问。示例如下:

public synchronized void method() {  
// 被锁定的方法  
}  

3)修饰静态方法:使用synchronized关键字修饰的静态方法,同样只能被一个线程访问。示例如下:

public static synchronized void method() {  
// 被锁定的静态方法  
}

4)修饰类:使用synchronized关键字修饰的类,同样只能被一个线程访问。示例如下:

public class MyClass {  
public void method() {  
synchronized(MyClass.class) {  
// 被锁定的代码块  
}  
}  
}

注意问题

使用synchronized关键字需要注意以下几点:

  • 1)粒度问题:要尽量缩小同步代码块的范围,以避免对程序性能的影响。

  • 2)死锁问题:如果在多个线程之间存在循环依赖的关系,就容易导致死锁问题。

  • 3)性能问题:每个线程获取锁和释放锁都需要消耗一定的时间,如果同步操作过于频繁,会严重影响程序的性能。

  • 4)可重入性问题:当一个线程已经获得了某个对象的锁之后,它可以再次进入该对象的同步代码块,而不需要重新获取锁。

volatile

volatile基本概念

并发编程的三大特性:
原子性、可见性和有序性。只要有一条原则没有被保证,就有可能会导致程序运行不正确。

  • 原子性:就是一个操作或多个操作中,要么全部执行,要么全部不执行

例如:账户A向账户B转账1000元,这个么过程涉及到两个操作,(1)A账户减去1000元 >(2)B账户增加1000元。这么两个操作必须具备原子性。否则A账户钱少了,B账户没增>加。

  • 有序性: 程序执行顺序按照代码先后顺序执行。

处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语»句的执行先后顺序同代码中的顺序一致(指令重排),但是它会保证程序最终执行结果»和代码顺序执行的结果是一致的。(此处的结果一致指的是在单线程情况下)

指令重排的理解:单线程侠,如果两个操作更换位置后,对后续操作结果没有影响,可以对这两个操作可以互换顺序。

  • 可见性: 可见性是指多线程共享一个变量,其中一个线程改变了变量值,其他线程能够立即看到修改的值。
  • volatile的作用

volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致»性问题。一个共享变量被 volatile关键字 修饰,那么就具备了两层语义:内存>可见性和禁止进行指令重排序。

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

CPU1执行线程1代码,CPU执行线程2代码。CPU读取i=0到CPU缓存中,修改i=10到自»己缓存,还没更新到主存,此时CPU2读取的i还是主存中i=0,此时j会被赋值为0;

volatile例子1

public class TestVisibility {

    //是否停止 变量
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {


        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();

        //休眠 10 毫秒
        Thread.sleep(10);
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
    
}

运行结果:
error

可见,线程1并不会停止,而是一直循环下去。这就是CPU缓存导致的一致性问题。

volatile例子2

public class Singleton {
    public static volatile Singleton singleton;

    /**
     * 构造函数私有,禁止外部实例化
     */
    private Singleton() {};

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (singleton) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:

  • 分配内存空间。
  • 初始化对象。
  • 将内存空间的地址赋值给对应的引用。
    但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
  • 分配内存空间。
  • 将内存空间的地址赋值给对应的引用。
  • 初始化对象
    如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导»致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量

transient

transient概念

Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输。一般地,当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,经常需要让实体类实现Serializable接口,目的就是为了让其可序列化。当然,序列化后的最终目的是为了反序列化,恢复成原先的Java对象实例。所以序列化后的字节序列都是可以恢复成Java对象的,这个过程就是反序列化 transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。 在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

transient例子

import java.io.Serializable;
 
public class User implements Serializable {
    private String name;
    /**
     * 密码不要被序列化  
     **/ 
    private transient String password;
 
    public User(String name, String password) {
        this.name = name;
        this.password = password;
    }
 
    public String getName() {
        return name;
    }
 
    public String getPassword() {
        return password;
    }
}
import java.io.*;
 
public class Test {
    public static void main(String[] args) throws Exception {
        User user = new User("Tom", "123456");
        System.out.println("用户名:" + user.getName() + ", 密码:" + user.getPassword());
 
        // 序列化对象
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.obj"));
        out.writeObject(user);
        out.close();
 
        // 反序列化对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.obj"));
        User newUser = (User) in.readObject();
        in.close();
 
        System.out.println("用户名:" + newUser.getName() + ", 密码:" + newUser.getPassword());
    }
}

error

native

native基本概念

Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “C”告知C++编译器去调用一个C的函数。
“A native method is a Java method whose implementation is provided by non-java code.”

package java.lang;
public class Object {
 ......
 public final native Class<?> getClass();
 public native int hashCode();
 protected native Object clone() throws CloneNotSupportedException;
 public final native void notify();
 public final native void notifyAll();
 public final native void wait(long timeout) throws InterruptedException;
 ......
}

native规则说明

  • 标识符native可以与所有其它的java标识符连用,但是abstract除外
  • 一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。
  • native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。
  • 一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪)

使用方法

JNI的书写步骤如下:

  • 编写带有native声明的方法的Java类
  • 使用javac命令编译编写的Java类
  • 使用java -jni ****来生成后缀名为.h的头文件
  • 使用其他语言(C、C++)实现本地方法
  • 将本地方法编写的文件生成动态链接库
  • 1编写
class HelloWorld{
public native void hello();
static{
    System.loadLibrary("hello");
}
public static void main(String[] args){
    new HelloWorld().hello();
}
}
  • 2 编译
javac HelloWorld.java
  • 3 生成h文件

第一个参数是调用JNI方法时使用的JNI Environment指针。第二个参数是指向在此Java代码中实例化的Java对象HelloWorld的一个句柄。其他参数是方法本身的参数

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*

* Class: HelloWorld
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

  • 4 实现C函数

其中,第一行是将jni.h文件引入(在%JAVA_HOME%\include目录下),里边有JNIEnv和jobject的定义。

#include <jni.h>
#include "HelloWorld.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_HelloWorld_hello(JNIEnv *env,jobject obj){
    printf("Hello World!\n");
    return;
}
  • 5 编译C实现

这里以在Windows中为例,需要生成dll文件。在保存HelloWorldImpl.c文件夹下面,使用VC的编译器cl成。

cl -I%java\_home%\\include -I%java\_home%\\include\\win32 -LD HelloWorldImp.c -Fehello.dll

注意:生成的dll文件名在选项-Fe后面配置,这里是hello,因为在HelloWorld.java文件中我们loadLibary的时候使用的名字是hello。当然这里修改之后那里也需要修改。另外需要将-I%java_home%\include -I%java_home%\include\win32参数加上,因为在第四步里面编写本地方法的时候引入了jni.h文件。

  • 6 运行程序
    java HelloWorld 运行程序 打印 “Hello World!”