Java学习笔记(二)

面向对象三大特性也不用多说了,首先是封装

Java 的封装和 C++ 的基本也相同,通过 public 和 private 关键字来实现,区别也在前面类那边提过了,除此之外还有个和 C++ 不同的就是,Java 一个文件里可以用多个类,但只能有一个类被 public 修饰,且这个类名必须和文件名相同,一般推荐是一个文件一个类

还有一个很重要的修饰符就是 static 修饰符,大体上也和 C++ 的相同,比如类共享,静态成员方法只能访问静态成员变量之类的,用法上唯一的不同就是 Java 没有类作用域界定符,所以访问静态成员变量和方法一般是通过类.静态成员变量/方法名

代码块

Java 中的类还有一个东西叫代码块,其实{}括起来的都可以算是代码块,除了普通的方法也算代码块之外,还有三种代码块

第一种叫静态代码块,执行优先度最高,在类加载的时候会运行一次,也只会运行这一次,比 main 方法都要高

class Test{
    static int cnt;
    public Test() {
        System.out.println("构造方法");
    }
    static {//静态块只会执行一次,一般用于初始化静态成员变量
        cnt = 0;
        System.out.println("静态块"+cnt);
    }
}

第二种叫构造代码块,在构造函数执行前执行,每次创建对象都会执行一次,用处不大

class Test{
    static int cnt;
    public Test() {
        System.out.println("构造方法");
    }
    {
        System.out.println("构造块");
    }
}

最后一种是同步代码块,用在多线程的时候

单例模式

很多时候我们需要某个类只能创建一个对象,而不能创建多个对象,这个设计模式就叫单例模式

单例模式有很多种,这边我只记录了比较简单的两种单例模式

首先很明显,要防止别人多次创建对象,自然要让构造函数私有,这是前提条件,其次就是要让这个唯一对象在一个不会收到影响的地方,显然不能用普通的成员变量来承载,因此需要用静态成员变量来存储这个唯一的对象

由此就产生了许多种单例模式,最简单的两种,我们称之为饿汉式和懒汉式

首先是饿汉式,思路最简单,就是初始化静态变量的时候,直接创建对象,如果需要防止这个唯一对象被删掉的话,可以把这个静态成员变量给设为 private,通过 public 的静态成员函数来访问

public class SingleObject {
 
   //创建 SingleObject 的一个对象
   private static SingleObject instance = new SingleObject();
 
   //让构造函数为 private,这样该类就不会被实例化
   private SingleObject(){}
 
   //获取唯一可用的对象
   public static SingleObject getInstance(){
      return instance;
   }
 
   public void showMessage(){
      System.out.println("Hello World!");
   }
}

饿汉式不仅简单也安全,唯一的缺点就是类被装载的时候就实例化了,如果后面一直不用的话,这部分内存就被浪费了

为了解决这个问题,就有了懒汉式,就是在第一使用的时候才创建这个对象,但是这种懒汉式在多线程的情况下,会失效,变得不安全,只能在单线程环境下使用,虽然可以通过 synchronized 修饰符来保证线程同步,但是效率太低,一般不用

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

继承

Java 中的继承大部分也是和 C++ 的情况相同,比如父类私有的无法访问,比如同名覆盖原则之类的

由于 Java 是没有类作用域界定符的,因此是不支持多继承的,想像 C++ 那样通过类作用域限定符来访问父类的东西时,需要使用 super 关键字

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
 
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    super.eat();  // super 调用父类方法
  }
}
 
public class Test {
  public static void main(String[] args) {
    Animal a = new Animal();
    a.eat();// animal : eat
    Dog d = new Dog();
    d.eat();    // dog : eat
    d.eatTest();// animal : eat
  }
}

Java 在同名覆盖原则上还有个特殊情况重写,可以通过在子类重写后的方法前面加上 @Override 注解符来让编译器进行更严格检查,同时可以增加可读性,C++ 其实也有类似的就是 override 关键字

重写的规则有以下几条

  • 重写方法的名称、形参列表必须与被重写方法的名称和参数列表一致
  • 私有的方法不能重写
  • 子类重写父类方法时,访问权限必须大于或等于父类(default < protected < public)
  • 子类不能重写父类的静态方法

重写的规则大致是和 C++ 的一样的,但 C++ 因为有虚函数的概念,java 的普通函数其实就已经是虚函数了,因为 Java 默认就是动态绑定的,因此 C++ 的重写还必须要求父类被重写的方法必须是虚函数,再由于 C++ 的虚函数实现是通过虚函数表的,因此原本的访问权限修饰符就没有什么影响,私有的方法也可以被重写,也不需要必须大于或等于父类

另外因为 Java 没有初始化列表这种东西,所以调用父类的构造函数,就必须在子类的构造函数中的第一行调用 super(),当然,如果没有加的话,编译器也会给你加上,但默认的只会调用父类的无参构造,如果父类没有无参构造函数就会报错。除此之外 Java 类中的 this 其实也可以调用本类的构造函数,用法和 super 一样,但是需要注意的是,这两个调用构造函数时,都必须放第一行,且不能同时使用

class SuperClass {
  private int n;
  SuperClass(){
    System.out.println("SuperClass()");
  }
  SuperClass(int n) {
    System.out.println("SuperClass(int n)");
    this.n = n;
  }
}
// SubClass 类继承
class SubClass extends SuperClass{
  private int n;
  
  SubClass(){ // 自动调用父类的无参数构造器
    System.out.println("SubClass");
  }  
  
  public SubClass(int n){ 
    super(300);  // 调用父类中带有参数的构造器
    System.out.println("SubClass(int n):"+n);
    this.n = n;
  }
}
// SubClass2 类继承
class SubClass2 extends SuperClass{
  private int n;
  
  SubClass2(){
    super(300);  // 调用父类中带有参数的构造器
    System.out.println("SubClass2");
  }  
  
  public SubClass2(int n){ // 自动调用父类的无参数构造器
    System.out.println("SubClass2(int n):"+n);
    this.n = n;
  }
}

最后一点就是 Java 所有类都有个祖先就是 Object,即便你没有继承其他任何类,也会默认继承 Object

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

包的作用

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

包的格式为

package pkg1[.pkg2[.pkg3…]];

一般为了保证包名不重复,我们会在自己定的包名前面加上自己的域名,来保证唯一,例如

com.tencent.mobileqq
com.tencent.wechat
net.java.util

对于在同一个包下面的所有类是可以直接访问的,如果想访问其他包下面的类,就需要导入了

import package1[.package2…].(classname|*);

如果一个类中需要访问多个不同的类,但这几个类的名称相同,那么默认就只能导入一个类,其他的类只能通过包名.类名来使用

访问权限

因为 Java 有包这个概念,所以 Java 的访问权限和 C++ 略有不同

大致如下

访问权限 同一个类中 同一个包中的其他类 不同包下的子类 不同包下的无关类
public
protected
default
private

final

Java 中虽然 有 const 这个关键字,但这个关键字只是保留的,目前并没有作用,但有个效果 和 C/C++ 的 const 相似的关键字就是 final

修饰变量时,final 和 C++ 的 const 相同,被修饰的变量有且仅能被赋值一次,如果修饰的是引用类型,则类似于 C++ 的常量指针,指向的地址不能被修改,但地址指向的对象可以被修改

在 Java 中 final 不仅仅可以修饰变量,还可以修饰类,表示这个不可被继承,同样的如果修饰的是方法,则表示这个方法不能被重写

常量

虽然 final 的效果就和 C++ 的 const 差不多了,不过在 Java 里常量其实指的是的被 pubic static final 修饰的成员变量,一般我们采用全大写字母,多个单词间使用下划线连接

常量在 Java 的编译过程中会进行 "宏替换",把使用常量的地方全部换成真实的字面量,因此 Java 中的常量更类似于 C/C++ 中的 #define 的效果,当然,C/C++ 的宏定义有其他更强大的作用(还有更大的坑)

枚举

Java 中的枚举是一种特殊的类,通过加 enum 关键字来使用,本质上是通过上面提到过的常量来实现的

enum Color 
{ 
    RED, GREEN, BLUE; 
} 
//反编译后为以下结果
public final class Color extends java.lang.Enum<Color> {
    public static final Color RED = new Color();
    public static final Color GREEN = new Color();
    public static final Color BLUE = new Color();
    public static Color[] values();
    public static Color valueOf(java.lang.String);
}

和 C++ 的 enum 有些不同,更接近于 C++ 的 enum class,C++ 的 enum 本质上是存储成整型的,而且作用域也是和普通变量一样的,且会发生隐式的类型转换,因此也很少使用了,现在一般都是使用强枚举类型 enum class,但和 java 的还是有一些区别,java 的 enum 就是一个特殊的类,因此是可以用自己的方法和变量的,不过构造函数必须私有,而 C++ 的 enum class 则无法添加函数

另外 java 的 switch 还有对枚举类的优化,如果 switch 的是个枚举对象,则 case 里可以省略类名,而C++ 则不行,必须用类名加作用域限定符

enum Color
{
    RED, GREEN, BLUE;
}
public class MyClass {
  public static void main(String[] args) {
    Color myVar = Color.BLUE;
​
    switch(myVar) {
      case RED:
        System.out.println("红色");
        break;
      case GREEN:
         System.out.println("绿色");
        break;
      case BLUE:
        System.out.println("蓝色");
        break;
    }
  }
}

抽象类

Java 中有 abstract 关键字来声明一个类是抽象类,抽象类无法被实例化,同样的 abstract 还可以用来修饰方法,这样的方法就叫做抽象方法,如果一个类有抽象方法就必须声明为抽象类,但抽象类可以没有抽象方法

这里的抽象方法,其实等同于 C++ 的纯虚函数,C++ 有纯虚函数的类就是抽象类,并没有 abstract 关键字

和 C++ 的纯虚函数一样,抽象方法必须全都被子类重写,否则这个子类就必须是抽象类,只不过 Java 需要显示声明这个类是抽象类,而 C++ 直接就变成了抽象类

另外需要注意的是 final 关键字和 abstract 关键字互斥,因为一个不可被继承和重写,另一个只有被继承和重写了才有意义

模板方法模式

这个同样是一种设计模式,在 C++ 里同样的思路也可以实现

定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

设计的方法就是在父类里写模板方法,也就是所有子类通用的部分,而把不通用的那部分提取出来,通过抽象方法来代替,在子类重写抽象方法,来实现各自不通用的那一部分

abstract class RefreshBeverage {
    /*
     * 封装所有子类共同遵循的算法框架
     */
    public final void RefreshBeverageTemplate() {
        System.out.println("把水煮沸");
        // 泡饮料
        brew();
        System.out.println("把饮料倒进杯子");
        // 加调味料
        addCondiments();
    }
    protected abstract void brew();
    protected abstract void addCondiments();
}
class Coffer extends RefreshBeverage {
​
    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡");
    }
​
    @Override
    protected void addCondiments() {
        // TODO Auto-generated method stub
        System.out.println("加糖和牛奶");
    }
​
}