Java的final关键字以及包装类

包装类

都说Java是面向对象的,一切都是对象,但是它依然提供了8种基本数据类型,这其实是为了照顾程序员的传统习惯,其实Java也为这8种基本数据类型提供了8个包装类,也就是将简单的数据类型包装成一个类来使用。

自动装箱与自动拆箱

将基本数据类型包装成包装类时叫装箱,将包装类拆成基本数据类型时叫拆箱,Java提供了自动装箱与自动拆箱功能,也就是说,可以直接将基本数据类型赋给一个包装类对象,也可以将一个包装类对象自动赋给一个基本数据类型。

package chap6;

public class AutoBoxingUnboxing {
  public static void main(String[] args) {
    Integer inObj = 5; // 自动装箱
    Object boolobj = true; // 自动装箱
    int it = inObj; //自动拆箱
    //当将一个父类对象强制转换为子类时,需要使用instanceof来判断,true才能转,否则会引发错误
    if(boolobj instanceof Boolean){
      boolean b = (Boolean)boolobj;
      System.out.println(b);
    }
  }
}

基本类型变量与字符串之间的转换

这是一个很常用的功能,也是一个很实际的功能。

String.valueof方法
primitive.valueof方法或者parseXXX方法
基本数据类型
String对象

最好记住valueof方法,实际上就是包装方法,可以实现基本数据和字符串类型之间的转换。

package chap6;

public class Primitive2String {
  public static void main(String[] args) {
    var intStr = "123";
    var it1 = Integer.parseInt(intStr);
    var it2 = Integer.valueOf(intStr);
    System.out.println("it1:"+it1);
    System.out.println("it2:"+it2);
    var ft1 = Float.parseFloat("4.56");
    var ft2 = Float.valueOf("4.56");
    System.out.println("ft1:"+ft1);
    System.out.println("ft2:"+ft2);
    var ftStr = String.valueOf(2.345f);
    var dbStr = String.valueOf(3.456);
    var boolStr = String.valueOf(true);
    System.out.println("ftStr:"+ftStr);
    System.out.println("dbStr:"+dbStr);
    System.out.println("boolStr:"+boolStr);
    var b = Boolean.valueOf("true");
    System.out.println("b:"+b);
    var c = Boolean.valueOf("123");
    var e = Boolean.valueOf("1");
    var f = Boolean.parseBoolean("1");
    System.out.println("c:"+c);
    System.out.println("e:"+e);
    System.out.println("f:"+f);
  }

}

包装类的实例可以直接和基本类型的值进行比较,这种比较是直接比较数值。但包装类的实例之间的比较就是另一种逻辑了。

包装类提供了一个静态方法。用于比较基本类型的值,第一个操作数大于第二个操作数则返回1,等于返回0,小于则返回-1。Boolean包装类也有这个方法,可以用其判断true和false的大小,true是大于false的。而布尔类型是不可以直接使用比较运算符来判断的。

    System.out.println(Boolean.compare(true, false)); // 1
    System.out.println(Boolean.compare(true, true)); // 0
    System.out.println(Integer.compare(3, 4)); // -1
    System.out.println(Integer.compare(4, 2)); // 1

处理对象

Java的对象都是Object的实例,都可以直接调用该类中的方法,而Object类中提供了一些处理Java对象的基本方法。

打印对象和toString()方法

如果直接使用System.out.println方法打印对象时,打印的是该对象在内存中的地址。Object提供的toString()方法也是这个作用。因此在定义自己的类时,需要重写这个方法,以满足特殊的需求。

==和equals()方法

Java程序中测试两个变量是否相等有两种方式:一种是使用==,另一种是使用equals()方法。

  • ==: 使用该符号去判断基本类型变量时,只要两个变量的值相等,那么就返回true,否则返回false。如果是引用变量之间使用等于符号去判断,则只有两个对象地址相等才会返回true,否则返回false。
  • equals方法:这个方法是Object类的一个成员方法,也就是说,所有对象均有这个方法。但Object中提供的equals方法和==的作用没什么区别。也就是说,需要自己去重写equals()方法,以满足特定的需求。

equals方法重写代码示例,重点注意equals()方法重写的技巧。

package chap6;

public class OverrideEqualsRight {
  public static void main(String[] args) {
    var p1 = new Person("ss","123");
    var p2 = new Person("sa","123");
    var p3 = new Person("ss","1234");
    System.out.println(p1.equals(p2)); // true
    System.out.println(p1.equals(p3)); // false

  }
}


class Person{
  private String name;
  private String idStr;
  public Person(){}
  public Person(String name, String idStr){
    this.name = name;
    this.idStr = idStr;
  }
  public String getName(){
    return this.name;
  }
  public String getIdStr(){
    return this.idStr;
  }
  public void setName(String name){
    this.name = name;
  }
  public void setIdStr(String idStr){
    this.idStr = idStr;
  }
  public boolean equals(Object o){
    //如果是同一个对象,则相等
    if(this == o)return true;
    //如果o不为空且类别是person类时,
    if(o != null && o.getClass() == Person.class){
      var personobj = (Person)o; // 这里就不需要使用instanceof 运算符来判断了,因为if已经判断过o是属于Person类了
      //再比较两个person对象的id是否相等,相等则这两个对象相等,否则不等
      return this.getIdStr().equals(personobj.getIdStr());
    }
    return false;
  }
}

Java程序中直接使用字符串常量 (包括在编译时就可以知道的字符串值)时,JVM会使用常量池来管理这些字符串。也就是说,当再次使用常量字符串时,就会使用常量池中的字符串,他们是同一个对象。如果使用new String(“疯狂Java”)这种方式来创建字符串对象,JVM会先用常量池来管理字符串常量,然后会在堆中重新创建一个对象。也就是说,产生了重复,会同时创建两个对象。,常量池不仅可以保存字符串常量,还可以保存其他类型的常量。

常量池

常量池实际上是一种缓存技术,在编译的时候能够确定下来的常量加入到常量池中,如果其他地方也用到这个常量,那么就不用重复去创建了。关键因素是,要在编译的时候就能知道,有可能是一个字符串直接两,也有可能是一个“宏变量”。

package chap6;

public class StringCompareTest {
  public static void main(String[] args) {
    // s1引用字符串常量池中的字符串
    var s1 = "疯狂Java";
    var s2 = "疯狂";
    var s3 = "Java";
    // s4和s5后面的字符串值可以在编译时就确定下来
    var s4 = "疯狂" + "Java";
    var s5 = "疯" + "狂" + "Java";
    // s6后面的值不能在编译时就确定下来,因此不能引用字符串常量池中的字符串
    var s6 = s2 + s3;
    // s7引用堆内存中新创建的String对象
    var s7 = new String("疯狂Java");
    System.out.println(s1 == s4); // true
    System.out.println(s1 == s5); // true
    System.out.println(s1 == s6); // false
    System.out.println(s1 == s7); // false
  }
}

String已经重写了Object的equals()方法:如果两个字符串所包含的字符序列相同,则返回true,否则返回false。

static关键字

之前已经反复提到过,在static的上下文中,不可以访问实例成员。原理很简单,因为实例成员需要对象来调用,而类成员(static)修饰的,是不依赖于对象的,因此有时候可能在未创建对象的时候就访问了实例成员,这显然是大大的错误。

单例类

在某些情况下要求一个类只能创建一个实例(比如只有一个设备),那么创建多个类也没有什么意义。但是通常构造器的修饰符是public,这意味着可以无限的创建对象,那么,怎么去限制一个类只能创建一个对象呢?首先要将构造器的修饰符改变为private,即不能让外界随便创建对象,然后再提供一个public修饰的方法,该方法的作用就是“包装”构造器,并且判断当前是否有了对象,有则返回当前对象,无则重新去创建对象。怎么去判断呢?实际上这里也需要缓存,也就是说,只创建一个实例,这个实例一经创建,就需要存储起来,以备后用。因此需要定义一个类变量来存储。示例代码如下:

package chap6;

public class SingletonTest {
  public static void main(String[] args) {
    Singleton s1 = Singleton.getInstance();
    Singleton s2 = Singleton.getInstance();
    System.out.println(s1 == s2);  //返回true,因为Singleton只能创建一个实例,叫单例类
  }
}

class Singleton{
  /*
  想要类只能创建一个对象,那么肯定要对构造器进行限制,将其设置为private,根据良好的封装原则,
  需要提供一个公共接口来创建对象,其只能是static的,因为构造器调用之前不会存在对象,
  封装起来后,还需要判断是否已经创建了对象,因此需要用一个变量来缓存,这个变量也只能是static的,因为在static
  修饰的方法中不能使用实例变量,因此在创建对象之前只需要判断这个变量是否为空
  为空,则可以创建,不为空,证明已经有了对象,便直接返回即可。
  这个缓存变量设置为private,其不能被外界访问,也不能被外界改变,
  它完全只在类中起作用。
   */
  private static Singleton instance;
  private Singleton(){} //将构造器隐藏,那么便不能自由创建对象了,在某些特殊场景下,需要这个限制
  public static Singleton getInstance(){ // 构造器被限制,那么就只能提供一个public修饰的方法来调用构造器了。
    /*
    这个方法只能是static的,因为构造器被隐藏了,调用该方法前是不存在对象的,因此只能设置为static
    除此之后,需要一个实例变量来缓存已经创建的对象,这个变量也只能是静态的,因为该
    变量需要被static方法访问
     */
    if(instance == null){  //为空才能创建对象
      instance = new Singleton();
    }
    return instance;
  }
}

final修饰符

final成员变量

由final修饰的变量是不可改变的,就是只能被赋值一次。成员变量可以被系统赋初值,可以被显式赋初值,也可以在构造器中赋初值。如果由final修饰的成员变量没有为其显式赋初值,则该变量会一直是系统默认的0,null,false等。这显然没有意义,因此Java语法规定:final修饰的成员变量(也包括类变量)必须显式的赋初值

final局部变量

局部变量和成员变量还是有些不同,局部变量必须显式初始化后才可以访问,因此final修饰的局部变量不一定非得在定义的时候就赋初值,也可以在其他地方赋值,但是一旦赋值之后,则不可以对其进行改变。

用final定义“宏变量”

满足3个条件,即相当于定义了一个“宏变量”:

  • 用final修饰
  • 定义的时候即显式赋初值
  • 该初值可以在编译时就确定下来

“宏变量”的意义在于,编译器在编译时就将程序中所有用到该变量的地方直接替换成值。

package chap6;

public class FinalReplaceTest {
  public static void main(String[] args) {
    final var a = 5+2;
    final var b = 1.2/3;
    final var str = "疯狂"+"java";
    final var book = "12"+98;
    final var book2 = "12" + String.valueOf(98);
    System.out.println(book == "1298"); //true
    System.out.println(book2 == "1298"); //false,book2并不在常量池中。 
  }
}

另一个关于final修饰符的程序:

package chap6;

public class StringJoinTest {
  public static void main(String[] args) {
    var s1 = "疯狂Java";
    var s2 = "疯狂"+"Java";
    System.out.println(s1==s2); //true
    var str1 = "疯狂";
    var str2 = "Java";
    var s3 = str1 + str2;
    System.out.println(s1==s3); //false
    //下面定义了宏变量
    final var str3 = "疯狂";
    final var str4 = "Java";
    var s4 = str3 + str4;  // 由于是宏变量,因此在编译的时候就可以知道其值。
    System.out.println(s1==s4); //true
  }
}

final方法

final修饰的方法不能被重写,但看下列代码:

package chap6;

public class PrivateFinalMethodTest {
  private final void test(){}
}

class Sub extends PrivateFinalMethodTest{
  public void test(){}  // 该方法定义没有问题,因为父类中的test方法是private权限,并不能构成方法重写。
}

这段代码是没有问题的,因为private权限已经将test方法封装了起来,并不能构成重写。

final类

final修饰的类不可以有子类。

不可变类

什么叫不可变类呢?就是当这个类创建实例之后,该实例的实例变量是不可改变的。
创建自己的不可变类,需遵守以下规则:

  • 使用private和final修饰类的成员变量
    这个要求很容易理解,既然是不可变类,那么就要实现良好的封装,且已经初始化就能被改变,因此要用到private和final关键字。
  • 提供带参数的构造器
    这个也是可以理解的,因为实例变量一经初始化就不能被改变,如果没有带参数的构造器,那么未免也太死板了一些。
  • 不要提供set方法,这很显然
  • 如有必要,重写equals方法和hashCode方法。
    由于是不可变类,有可能equals方法需要比较特定实例变量来判断对象是否相等。

实例变量为基本类型的不可变类很好实现,只需要用final修饰即可。如下:

package chap6;

public class Address {
  private final String detail; // 定义为final变量,即不可改变
  private final String postCode;
  public Address(String detail, String postCode){
    this.detail = detail;
    this.postCode = postCode;
  }
  public String getDetail(){
    return this.detail;
  }
  public String getPostCode(){
    return this.postCode;
  }

  public boolean equals(Object o){  // 重写equals方法
    if(this == o)return true;
    if(o!=null && Address.class == o.getClass()){
      var ad = (Address)o;
      if(this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode())){
        return true;
      }
    }
    return false;
  }

  public int hashCode(){
    return this.detail.hashCode() + this.postCode.hashCode()*31;
  }

}

但是如果实例变量是引用类型的话,则需要注意,如果引用的变量本身就不可变,比如String类型,那么就和基本类型变量的情况一样。如果引用的变量可变,那就得注意了,比如:

package chap6;


class Name{
  private String firstname;
  private String lastname;
  public Name(){}
  public Name(String firstname, String lastname){
    this.firstname = firstname;
    this.lastname = lastname;
  }
  public void setFirstname(String firstname){
    this.firstname = firstname;
  }
  public void setLastname(String lastname){
    this.lastname = lastname;
  }
  public String getFirstname(){
    return this.firstname;
  }
  public String getLastname(){
    return this.lastname;
  }
}

class Person2{
   private final Name name;

   /*
   由于成员变量是引用类型,这个时候就要注意了,因为final修饰引用类型的变量只能保证引用的地址不会发生改变
   但是地址内的内容是完全可以发生改变的,比如上面的Name类,这个时候构造器不能直接返回name了
   应该返回一个匿名对象,保证它的firstname和lastname与给定的name相同即可。
   相当于Person2类中的name的对象与给定的name对象不是同一个对象,其地址是不同的
    */
   public Person2(Name name){
     this.name = new Name(name.getFirstname(), name.getLastname());
   }
   /*
   同理,get函数也不可直接返回name,因为一旦被外界获取,还是可以被外界修改的
   因此也仿照构造器的样子,返回一个匿名对象,使得内部的name对象被保护起来
    */
   public Name getName(){
     return new Name(name.getFirstname(), name.getFirstname());
   }
}
public class Person1 {
  private final Name name;
  public Person1(Name name){
    this.name = name;
  }
  public Name getName() {
    return this.name;
  }

  @Override
  public boolean equals(Object obj) {
    if(this == obj) return true;

    if(obj != null && obj.getClass() == Person1.class){
      var p = (Person1)obj;
      return this.name.getFirstname().equals(p.getName().getFirstname()) && this.name.getLastname().equals(p.getName().getLastname());
    }
    return false;
  }



  public static void main(String[] args) {
    var n = new Name("xx", "ss");
    var p = new Person1(n);
    var p2 = new Person2(n);
    System.out.println(p2.getName().getFirstname()); // xx
    System.out.println(p.getName().getFirstname()); // xx
    n.setFirstname("aa");
    System.out.println(p.getName().getFirstname()); // aa, 可见,不可变类的成员变量也发生了改变
    System.out.println(p2.getName().getFirstname()); // xx ,没有发生改变,可见不可变类创建成功

  }
}

缓存实例得不可变类

如下代码,这也是体会面向对象得一个好的例子,从下列代码也可以看出,不可变类不一定要求所有得实例变量都不可变,只要保证“核心实例变量”不可变即可。

package chap6;

public class CacheImmutaleTest {
  /*
  好好体会以一下面向对象的思想,在Java中,都需要对象,都需要类
  只是想实现一个缓存的功能,如果按照c语言的想法,定义一个函数,里面定义数组,然后加上判断逻辑即可
  但是这时Java,因此首先要创建一个缓存类,类中有数组,name,pos,MAZ_SIZE这些实例变量
  但是真正重要的是name变量,因为是要实现name变量的缓存,因此数组,pos,MAX_SIZE这些变量都是给name服务的
  相当于将name包装了,实现了缓存的功能。
   */
  private static int MAX_SIZE = 10;
  private static CacheImmutaleTest[] cache = new CacheImmutaleTest[MAX_SIZE];
  private static int pos = 0;
  private final String name; // 核心实例变量,显然需要封装
  /*
  将构造器进行封装,那么就意味着只能通过valueof方法来创建对象了,
  也意味着总是要使用缓存,这在某些情况下可能不好,因为缓存是要占据空间的,
  但其实如果不想缓存,直接定义一个final修饰的String类型字符串即可。
   */
  private CacheImmutaleTest(String name){
    this.name = name;
  }

  // 也需要为其提供get方法
  public String getName(){
    return this.name;
  }

  /*
  实现缓存的主要函数:对于一个新的字符串,先查找是否已经存在
  存在则返回,不存在则看存储是否满了。满了则将第一个元素覆盖,pos重置为1,即采用“先进先出”的原则
  否则直接新建再存储。
   */
  public static CacheImmutaleTest valueof(String name){
    for(var i=0; i< MAX_SIZE;i++){
      if(cache[i] != null && cache[i].getName().equals(name)){
        return cache[i];
      }
    }
    if(pos == MAX_SIZE){
      cache[0] = new CacheImmutaleTest(name);
      pos = 1;
    }
    else{
      cache[pos++] = new CacheImmutaleTest(name);
    }
    return cache[pos-1];
  }

  //equals方法也是根据name来判断
  public boolean equals(Object obj){
    if(this == obj)return true;
    if(obj != null && obj.getClass() == CacheImmutaleTest.class){
      var ca = (CacheImmutaleTest)obj;
      return this.name.equals(ca.getName());
    }
    return false;
  }

  // hash也是根据name的hash,可见“核心实例变量是name”
  public int hashCode(){
    return this.name.hashCode();
  }

  public static void main(String[] args) {
    var c1 = CacheImmutaleTest.valueof("hello");
    var c2 = CacheImmutaleTest.valueof("hello");
    System.out.println(c1 == c2); // true
  }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值