java面向对象(包装类、toString、==和equals、单例类、final)

1 Java 7增强的包装类

但在某些时候,基本数据类型会有一些制约,例如所有引用类型的变量都继承了Object类,都可当成Object类型变量使用。但基本数据类型的变量就不可以,如果有个方法需要Object类型的参数,但实际需要的值却是2、3等数值,这可能就比较难以处理。
在这里插入图片描述
8个包装类中除了Character之外,还可以通过传入一个字符串参数来构建包装类对象。

        public class Primitive2Wrapper
        {
            public static void main(String[] args)
            {
                  boolean bl=true;
                  //通过构造器把b1基本类型变量包装成包装类对象
                  Boolean blObj=new Boolean(bl);
                  int it=5;
                  //通过构造器把it基本类型变量包装成包装类对象
                  Integer itObj=new Integer(it);
                  //把一个字符串转换成Float对象
                  Float fl=new Float("4.56");
                  //把一个字符串转换成Boolean对象
                  Boolean bObj=new Boolean("false");
                  //下面程序运行时将出现java.lang.NumberFormatException异常
                  //Long lObj=new Long("ddd");
            }
        }

如果希望获得包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法。

        //取出Boolean对象里的boolean变量
        boolean bb=bObj.booleanValue();
        //取出Integer对象里的int变量
        int i=itObj.intValue();
        //取出Float对象里的float变量
        float f=fl.floatValue();

基本类型变量和包装类对象之间的转换关系
在这里插入图片描述
JDK 1.5提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing) 功能。所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量);自动拆箱则与之相反,允许直接把包装类对象直接赋给一个对应的基本类型变量。

        public class AutoBoxingUnboxing
        {
            public static void main(String[] args)
            {
                  //直接把一个基本类型变量赋给Integer对象
                  Integer inObj=5;
                  //直接把一个boolean类型变量赋给一个Object类型变量
                  Object boolObj=true;
                  //直接把一个Integer对象赋给int类型变量
                  int it=inObj;
                  if (boolObj instanceof Boolean)
                  {
                        //先把Object对象强制类型转换为Boolean类型,再赋给boolean变量
                        boolean b=(Boolean)boolObj;
                        System.out.println(b);
                  }
            }
        }

包装类还可实现基本类型变量和字符串之间的转换。
字符串类型的值转换为基本类型的值有两种方式。

  • 利用包装类提供的parseXxx(String s)静态方法(除了Character之外的所有包装类都提供了该方法
  • 利用包装类提供的Xxx(String s)构造器

String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串

        public class Primitive2String
        {
            public static void main(String[] args)
            {
                  String intStr="123";
                  //把一个特定字符串转换成int变量
                  int it1=Integer.parseInt(intStr);
                  int it2=new Integer(intStr);
                  System.out.println(it2);
                  String floatStr="4.56";
                  //把一个特定字符串转换成float变量
                  float ft1=Float.parseFloat(floatStr);
                  float ft2=new Float(floatStr);
                  System.out.println(ft2);
                  //把一个float变量转换成String变量
                  String ftStr=String.valueOf(2.345f);
                  System.out.println(ftStr);
                  //把一个double变量转换成String变量
                  String dbStr=String.valueOf(3.344);
                  System.out.println(dbStr);
                  //把一个boolean变量转换成String变量
                  String boolStr=String.valueOf(true);
                  System.out.println(boolStr.toUpperCase());
            }
        }

基本类型变量和字符串之间的转换关系
在这里插入图片描述
如果希望把基本类型变量转换成字符串,还有一种更简单的方法:将基本类型变量""进行连接运算,系统会自动把基本类型变量转换成字符串。

虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较,这种比较是直接取出包装类实例所包装的数值来进行比较的。

        Integer a=new Integer(6);
        //输出true
        System.out.println("6的包装类实例是否大于5.0" + (a > 5.0));

两个包装类的实例进行比较的情况就比较复杂,因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回true。

        //输出false
        System.out.println("比较2个包装类的实例是否相等:"
            + (new Integer(2)==new Integer(2)));
        //通过自动装箱,允许把基本类型值赋值给包装类实例
        Integer ina=2;
        Integer inb=2;
        //输出true
        System.out.println("两个2自动装箱后是否相等:" + (ina==inb));
        Integer biga=128;
        Integer bigb=128;
        //输出false
        System.out.println("两个128自动装箱后是否相等:" + (biga==bigb));

同样是两个int类型的数值自动装箱成Integer实例后,如果是两个2自动装箱后就相等;但如果是两个128自动装箱后就不相等,这是为什么呢?这与Java的Integer类的设计有关,

            //定义一个长度为256的Integer数组
            static final Integer[] cache=new Integer[-(-128) + 127 + 1];
            static {
                //执行初始化,创建-128到127的Integer实例,并放入cache数组中
                for(int i=0; i < cache.length; i++)
                      cache[i]=new Integer(i - 128);
            }

系统把一个-128~127之间的整数自动装箱成Integer实例,并放入了一个名为cache的数组中缓存起来。如果以后把一个-128~127之间的整数自动装箱成一个Integer实例时,实际上是直接指向对应的数组元素,因此-128~127之间的同一个整数自动装箱成Integer实例时,永远都是引用cache数组的同一个数组元素,所以它们全部相等;但每次把一个不在-128~127范围内的整数自动装箱成Integer实例时,系统总是重新创建一个Integer实例,所以出现程序中的运行结果。

Java为什么要对这些数据进行缓存呢?

答:缓存是一种非常优秀的设计模式,在Java、Java EE平台的很多地方都会通过缓存来提高系统的运行性能。简单地说,如果你需要一台电脑,那么你就去买了一台电脑。但你不可能一直使用这台电脑,你总会离开这台电脑——在你离开电脑的这段时间内,你如何做?你会不会立即把电脑扔掉?当然不会,你会把电脑放在房间里,等下次又需要电脑时直接开机使用,而不是再次去购买一台。假设电脑是内存中的对象,而你的房间是内存,如果房间足够大,则可以把所有曾经用过的各种东西都缓存起来,但这不可能,房间的空间是有限制的,因此有些东西你用过一次就扔掉了。你只会把一些购买成本大、需要频繁使用的东西保存下来。类似地,Java也把一些创建成本大、需要频繁使用的对象缓存起来,从而提高程序的运行性能。

Java 7为所有的包装类都提供了一个静态的compare(xxx val1, xxx val2)方法,这样开发者就可以通过包装类提供的compare(xxx val1, xxx val2)方法来比较两个基本类型值的大小,包括比较两个boolean类型值,两个boolean类型值进行比较时,true > false

        System.out.println(Boolean.compare(true , false));  //输出1
        System.out.println(Boolean.compare(true , true));  //输出0
        System.out.println(Boolean.compare(false , true));  //输出-1

2 处理对象

Java对象都是Object类的实例,都可直接调用该类中定义的方法,这些方法提供了处理Java对象的通用方法。

2.1 打印对象和toString方法

        class Person
        {
            private String name;
            public Person(String name)
            {
                  this.name=name;
            }
            public void info()
            {
                  System.out.println("此人名为:" + name);
            }
        }
        public class PrintObject
        {
            public static void main(String[] args)
            {
                  //创建一个Person对象,将之赋给p变量
                  Person p=new Person("孙悟空");
                  //打印p所引用的Person对象
                  System.out.println(p);
            }
        }

运行结果:

            Person@f72617

System.out.println方法只能在控制台输出字符串,而Person实例是一个内存中的对象,当使用该方法输出Person对象时,实际上输出的是Person对象的toString()方法的返回值

下面两行代码的效果完全一样。

        System.out.println(p);
        System.out.println(p.toString());

不仅如此,所有的Java对象都可以和字符串进行连接运算,当Java对象和字符串进行连接运算时,系统自动调用Java对象toString方法的返回值字符串进行连接运算,即下面两行代码的结果也完全相同。

        String pStr=p + "";
        String pStr=p.toString() + "";

Object类提供的toString方法总是返回该对象实现类的“类名+@+hashCode”值,这个返回值并不能真正实现“自我描述”的功能,因此如果用户需要自定义类能实现“自我描述”的功能,就必须重写Object类的toString方法。

2.2 ==和equals方法

但对于两个引用类型变量,它们必须指向同一个对象时,==判断才会返回true==不可用于比较类型上没有父子关系的两个对象。

        public class EqualTest
        {
            public static void main(String[] args)
            {
                  int it=65;
                  float fl=65.0f;
                  //将输出true
                  System.out.println("65和65.0f是否相等?" + (it==fl));
                  char ch='A';
                  //将输出true
                  System.out.println("65和'A'是否相等?" + (it==ch));
                  String str1=new String("hello");
                  String str2=new String("hello");
                  //将输出false
                  System.out.println("str1和str2是否相等?"
                        + (str1==str2));
                  //将输出true
                  System.out.println("str1是否equals str2?"
                        + (str1.equals(str2)));
                  //由于java.lang.String与EqualTest类没有继承关系
                  //所以下面语句导致编译错误
                  System.out.println("hello"==new EqualTest());
            }
        }

"hello"直接量和new String(“hello”)有什么区别呢?
当Java程序直接使用形如 "hello"的字符串直接量(包括可以在编译时就计算出来的字符串值)时,JVM将会使用常量来管理这些字符串;
当使用new String(“hello”) 时,JVM会先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。换句话说,new String(“hello”)一共产生了两个对象。

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

        public class StringCompareTest
        {
            public static void main(String[] args)
            {
                  //s1直接引用常量池中的"疯狂Java"
                  String s1="疯狂Java";
                  String s2="疯狂";
                  String s3="Java";
                  //s4后面的字符串值可以在编译期就确定下来
                  //s4直接引用常量池中的"疯狂Java"
                  String s4="疯狂" + "Java";
                  //s5后面的字符串值可以在编译期就确定下来
                  //s5直接引用常量池中的"疯狂Java"
                  String s5="疯" + "狂" + "Java";
                  //s6后面的字符串值不能在编译期就确定下来
                  //不能引用常量池中的字符串
                  String s6=s2 + s3;
                  //使用new调用构造器将会创建一个新的String对象
                  //s7引用堆内存中新创建的String对象
                  String s7=new String("疯狂Java");
                  //输出true
                  System.out.println(s1==s4);
                  //输出true
                  System.out.println(s1==s5);
                  //输出false
                  System.out.println(s1==s6);
                  //输出false
                  System.out.println(s1==s7);
            }
        }

JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。例子中的s1、s4、s5所引用的字符串可以在编译期就确定下来,因此它们都将引用常量池中的同一个字符串对象。

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

但在很多时候,程序判断两个引用变量是否相等时,也希望有一种类似于“值相等”的判断规则,并不严格要求两个引用变量指向同一个对象。例如对于两个字符串变量,可能只是要求它们引用字符串对象里包含的字符序列相同即可认为相等。此时就可以利用String对象的equals方法来进行判断,例如上面程序中的str1.equals(str2)将返回true。

equals方法是Object类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量相等。但使用这个方法判断两个对象相等的标准与使用==运算符没有区别,同样要求两个引用变量指向同一个对象才会返回true。因此这个Object类提供的equals方法没有太大的实际意义,如果希望采用自定义的相等标准,则可采用重写equals方法来实现。

提示:String已经重写了Objectequals() 方法,Stringequals() 方法判断两个字符串相等的标准是:只要两个字符串所包含的字符序列相同,通过equals()比较将返回true,否则将返回false。

重写equals方法就是提供自定义的相等标准,你认为怎样是相等,那就怎样是相等,一切都是你做主!

通常而言,正确地重写equals方法应该满足下列条件。

  • 自反性:对任意x,x.equals(x)一定返回true。
  • 对称性:对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
  • 传递性:对任意x, y,
    z,如果x.equals(y)返回ture,y.equals(z)返回true,则x.equals(z)一定返回true。
  • 一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。
  • 对任何不是null的x,x.equals(null)一定返回false。

Object默认提供的equals()只是比较对象的地址,即Object类equals方法比较的结果与==运算符比较的结果完全相同。

下面重写Person类的equals方法

        class Person
        {
            private String name;
            private String idStr;
            public Person(){}
            public Person(String name , String idStr)
            {
                  this.name=name;
                  this.idStr=idStr;
            }
            //此处省略name和idStr的setter和getter方法
            //重写equals方法,提供自定义的相等标准
            public boolean equals(Object obj)
            {
                  // 如果两个对象为同一个对象
                  if (this==obj)
                        return true;
                  //当obj不为null,且它是Person类的实例时
                  if (obj !=null && obj.getClass()==Person.class)
                  {
                        Person personObj=(Person)obj;
                        //并且当前对象的idStr与obj对象的idStr相等才可判断两个对象相等
                        if (this.getIdStr().equals(personObj.getIdStr()))
                        {
                            return true;
                        }
                  }
                  return false;
            }
        }
        public class OverrideEqualsRight
        {
            public static void main(String[] args)
            {
                  Person p1=new Person("孙悟空" , "12343433433");
                  Person p2=new Person("孙行者" , "12343433433");
                  Person p3=new Person("孙悟饭" , "99933433");
                  //p1和p2的idStr相等,所以输出true
                  System.out.println("p1和p2是否相等?"
                        + p1.equals(p2));
                  //p2和p3的idStr不相等,所以输出false
                  System.out.println("p2和p3是否相等?"
                        + p2.equals(p3));
            }
        }

提问:判断obj是否为Person类的实例时,为何不用obj instanceof Person来判断呢?

对于instanceof运算符而言,当前面对象是后面类的实例或其子类的实例时都将返回true,所以实际上重写equals()方法判断两个对象是否为同一个类的实例时使用instanceof是有问题的。比如有一个Teacher类型的变量t,如果判断t instanceof Person,这也将返回true。但对于重写equals()方法的要求而言,通常要求两个对象是同一个类的实例,因此使用instanceof运算符不太合适。改为使用t.getClass()==Person.class比较合适。

3 类成员

3.1 理解类成员

当使用实例来访问类成员时,实际上依然是委托给该类来访问类成员,因此即使某个实例null,它也可以访问它所属类的类成员

        public class NullAccessStatic
        {
            private static void test()
            {
                  System.out.println("static修饰的类方法");
            }
            public static void main(String[] args)
            {
                  //定义一个NullAccessStatic变量,其值为null
                  NullAccessStatic nas=null;
                  //null对象调用所属类的静态方法
                  nas.test();
            }
        }

编译、运行上面程序,一切正常,程序将打印出“静态方法”字符串,这表明null对象可以访问它所属类的类成员。

如果一个null对象访问实例成员(包括Field方法),将会引发NullPointerException异常,因为null表明该实例根本不存在,既然实例不存在,理所当然的,它的Field和方法也不存在。

3.2 单例(Singleton)类

但在某些时候,允许其他类自由创建该类的对象没P有任何意义,还可能造成系统性能下降(因为频繁地创建对象、回收对象带来的系统开销问题)。例如,系统可能只有一个窗口管理器、一个假脱机打印设备或一个数据库引擎访问点,此时如果在系统中为这些类创建多个对象就没有太大的实际意义。

如果一个类始终只能创建一个实例,则这个类被称为单例类

为了避免其他类自由创建该类的实例,我们把该类的构造器使用private修饰,从而把该类的所有构造器隐藏起来。

根据良好封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。

除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用static修饰。

        class Singleton
        {
            //使用一个变量来缓存曾经创建的实例
            private static Singleton instance;
            //对构造器使用private修饰,隐藏该构造器
            private Singleton(){}
            //提供一个静态方法,用于返回Singleton实例
            //该方法可以加入自定义控制,保证只产生一个Singleton对象
            public static Singleton getInstance()
            {
                  //如果instance为null,则表明还不曾创建Singleton对象
                  //如果instance不为null,则表明已经创建了Singleton对象
                  //将不会重新创建新的实例
                  if (instance==null)
                  {
                        //创建一个Singleton对象,并将其缓存起来
                        instance=new Singleton();
                  }
                  return instance;
            }
        }
        public class SingletonTest
        {
            public static void main(String[] args)
            {
                  //创建Singleton对象不能通过构造器
                  //只能通过getInstance方法来得到实例
                  Singleton s1=Singleton.getInstance();
                  Singleton s2=Singleton.getInstance();
                  //将输出true
                  System.out.println(s1==s2);
            }
        }

4 final修饰符

final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例Z变量),也可以修饰局部变量、形参。
final修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值。

4.1 final成员变量

当执行静态初始化块时可以对类Field赋初始值;当执行普通初始化块、构造器时可对实例Field赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。

对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、‘\u0000’、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。

        public class FinalVariableTest
        {
            //定义成员变量时指定默认值,合法
            final int a=6;
            //下面变量将在构造器或初始化块中分配初始值
            final String str;
            final int c;
            final static double d;
            //既没有指定默认值,又没有在初始化块、构造器中指定初始值
            //下面定义char Field是不合法的
            //final char ch;
            //初始化块,可对没有指定默认值的实例Field指定初始值
            {
                  //在初始化块中为实例Field指定初始值,合法
                  str="Hello";
                  //定义a Field时已经指定了默认值
                  //不能为a重新赋值,下面赋值语句非法
                  //a=9;
            }
            //静态初始化块,可对没有指定默认值的类Field指定初始值
            static
            {
                  //在静态初始化块中为类Field指定初始值,合法
                  d=5.6;
            }
            //构造器,可对既没有指定默认值,又没有在初始化块中
            //指定初始值的实例Field指定初始值
            public FinalVariableTest()
            {
                  //如果初始化块中对str指定了初始值
                  //则构造器中不能对final变量重新赋值,下面赋值语句非法
                  //str="java";
                  c=5;
            }
            public void changeFinal()
            {
                  //普通方法不能为final修饰的成员变量赋值
                  //d=1.2;
                  //不能在普通方法中为final成员变量指定初始值
                  //ch='a';
            }
            public static void main(String[] args)
            {
                  FinalVariableTest ft=new FinalVariableTest();
                  System.out.println(ft.a);
                  System.out.println(ft.c);
                  System.out.println(ft.d);
            }
        }

与普通成员变量不同的是,final成员变量(包括实例Field和类Field)必须由程序员显式初始化,系统不会对final成员进行隐式初始化。

4.2 final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值

        public class FinalLocalVariableTest
        {
            public void test(final int a)
            {
                  //不能对final修饰的形参赋值,下面语句非法
                  //a=5;
            }
            public static void main(String[] args)
            {
                  //定义final局部变量时指定默认值,则str变量无法重新赋值
                  final String str="hello";
                  //下面赋值语句非法
                  //str="Java";
                  //定义final局部变量时没有指定默认值,则d变量可被赋值一次
                  final double d;
                  //第一次赋初始值,成功
                  d=5.6;
                  //对final变量重复赋值,下面语句非法
                  //d=3.4;
            }
        }

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

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变

        class Person
        {
            private int age;
            public Person(){}
            //有参数构造器
            public Person(int age)
            {
                  this.age=age;
            }
            //省略age Field的setter和getter方法
            ...
        }
        public class FinalReferenceTest
        {
            public static void main(String[] args)
            {
                  //final修饰数组变量,iArr是一个引用变量
                  final int[] iArr={5, 6, 12, 9};
                  System.out.println(Arrays.toString(iArr));
                  //对数组元素进行排序,合法
                  Arrays.sort(iArr);
                  System.out.println(Arrays.toString(iArr));
                  //对数组元素赋值,合法
                  iArr[2]=-8;
                  System.out.println(Arrays.toString(iArr));
                  //下面语句对iArr重新赋值,非法
                  //iArr=null;
                  //final修饰Person变量,p是一个引用变量
                  final Person p=new Person(45);
                  //改变Person对象的age Field,合法
                  p.setAge(23);
                  System.out.println(p.getAge());
                  //下面语句对p重新赋值,非法
                  //p=null;
            }
        }

从上面程序中可以看出,使用final修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。

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

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

  1. 使用final修饰符修饰;
  2. 在定义该final变量时指定了初始值;
  3. 该初始值可以在编译时就被确定下来。
        public class FinalLocalTest
        {
            public static void main(String[] args)
            {
                  //定义一个普通局部变量
                  final int a=5;
                  System.out.println(a);
            }
        }

对于这个程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为执行System.out.println(5)。

注意:
final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

除了上面那种为final变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理

        public class FinalReplaceTest
        {
            public static void main(String[] args)
            {
                  //下面定义了4个final“宏变量”
                  final int a=5 + 2;
                  final double b=1.2 / 3;
                  final String str="疯狂" + "Java";
                  final String book="疯狂Java讲义:" + 99.0;
                  //下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
                  final String book2="疯狂Java讲义:" + String.valueOf(99.0);  //①
                  System.out.println(book=="疯狂Java讲义:99.0");
                  System.out.println(book2=="疯狂Java讲义:99.0");
            }
        }

Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a=“java”;语句之后,系统的字符串池中就会缓存一个字符串" java ";如果程序再次执行String b=“java”;,系统将会让b直接指向字符串池中的"java"字符串,因此a==b将会返回true。

        public class StringJoinTest
        {
            public static void main(String[] args)
            {
                  String s1="疯狂Java";
                  //s2变量引用的字符串可以在编译时就确定下来
                  //因此引用常量池中已有的"疯狂Java"字符串
                  String s2="疯狂" + "Java";
                  System.out.println(s1==s2);
                  //定义2个字符串直接量
                  String str1="疯狂";    //①
                  String str2="Java";    //②
                  //将str1和str2进行连接运算
                  String s3=str1 + str2;
                  System.out.println(s1==s3);
            }
        }

让s1==s3输出true也很简单,只要让编译器可以对str1、str2两个变量执行“宏替换”,这样编译器即可在编译阶段就确定s3的值,就会让s3指向字符串池中缓存的“疯狂Java”。也就是说,只要将①、②两行代码所定义的str1、str2使用final修饰即可。

4.5 final方法

final修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。

Java提供的Object类里就有一个final方法:getClass(),因为Java不希望任何类重写这个方法,所以使用final把这个方法密封起来。但对于该类提供的toString()equals()方法,都允许子类重写,因此没有使用final修饰它们。

对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。

    public class PrivateFinalMethodTest
    {
        private final void test(){}
    }
    class Sub extends PrivateFinalMethodTest
    {
        //下面的方法定义不会出现问题
        public void test(){}
    }

final修饰的方法仅仅是不能被重写,并不是不能被重载

    public class FinalOverload
    {
        //final修饰的方法只是不能被重写,完全可以被重载
        public final void test(){}
        public final void test(String arg){}
    }

4.6 final类

final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。

            public final class FinalClass {}
            //下面的类定义将出现编译错误
            class Sub extends FinalClass {}

4.7 不可变类

不可变(immutable)类的意思是创建该类的实例后,该实例的Field是不可改变的。

        Double d=new Double(6.5);
        String str=new String("Hello");

上面程序创建了一个Double对象和一个String对象,并为这个两对象传入了6.5和"Hello"字符串作为参数,那么Double类和String类肯定需要提供实例Field来保存这两个参数,但程序无法修改这两个实例Field值,因此Double类和String类没有提供修改它们的方法。

如果需要创建自定义的不可变类,可遵守如下规则。

  • 使用private和final修饰符来修饰该类的Field。
  • 提供带参数构造器,用于根据传入参数来初始化类里的Field。
  • 仅为该类的Field提供getter方法,不要为该类的Field提供setter方法,因为普通方法无法修改final修饰的Field。
  • 如果有必要,重写Object类的hashCode和equals方法。equals方法以关键Field来作为判断两个对象是否相等的标准,除此之外,还应该保证两个用equals方法判断为相等的对象的hashCode也相等。

例如,java.lang.String这个类就做得很好,它就是根据String对象里的字符序列来作为相等的标准,其hashCode方法也是根据字符序列计算得到的。

        public class ImmutableStringTest
        {
            public static void main(String[] args)
            {
                  String str1=new String("Hello");
                  String str2=new String("Hello");
                  //输出false
                  System.out.println(str1==str2);
                  //输出true
                  System.out.println(str1.equals(str2));
                  //下面两次输出的hashCode相同
                  System.out.println(str1.hashCode());
                  System.out.println(str2.hashCode());
            }
        }

与不可变类对应的是可变类,可变类的含义是该类的实例Field是可变的。大部分时候所创建的类都是可变类,特别是JavaBean,因为总是为其Field提供了settergetter方法。

前面介绍final关键字时提到,当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可改变。这就产生了一个问题:当创建不可变类时,如果它包含Field的类型是可变的,那么其对象的Field值依然是可改变的——这个不可变类其实是失败的。

下面程序试图定义一个不可变的Person类,但因为Person类包含一个引用类型Field,且这个引用类是可变类,所以导致Person类也变成了可变类。

        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 String getFirstName()
            {
                  return this.firstName;
            }
            public void setLastName(String lastName)
            {
                  this.lastName=lastName;
            }
            public String getLastName()
            {
                  return this.lastName;
            }
        }
        public class Person
        {
            private final Name name;
            public Person(Name name)
            {
                  this.name=name;
            }
            public Name getName()
            {
                  return name;
            }
            public static void main(String[] args)
            {
                  Name n=new Name("悟空", "孙");
                  Person p=new Person(n);
                  // Person对象的name的firstName值为"悟空"
                  System.out.println(p.getName().getFirstName());
                  // 改变Person对象的name的firstName值
                  n.setFirstName("八戒");
                  // Person对象的name的firstName值被改为"八戒"
                  System.out.println(p.getName().getFirstName());
            }
        }

为了保持Person对象的不可变性,必须保护好Person对象的引用类型Field:name,让程序无法访问到Person对象的name Field,也就无法利用name Field的可变性来改变Person对象了。为此,我们将Person类改为如下

        public class Person
        {
            private final Name name;
            public Person(Name name)
            {
                  //设置name Field为临时创建的Name对象,该对象的firstName和lastName
                  //与传入的name对象的firstName和lastName相同
                  this.name=new Name(name.getFirstName(), name.getLastName());
            }
            public Name getName()
            {
                  //返回一个匿名对象,该对象的firstName和lastName
                  //与该对象里的name的firstName和lastName相同
                  return new Name(name.getFirstName(), name.getLastName());
            }
        }

当Person对象返回name Field时,它并没有直接把name Field返回,直接返回name Field的值也可能导致它所引用的Name对象被修改。

4.8 缓存实例的不可变类

不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。

本节将使用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。

        class CacheImmutale
        {
            private static int MAX_SIZE=10;
            //使用数组来缓存已有的实例
            private static CacheImmutale[] cache
                =new CacheImmutale[MAX_SIZE];
            //记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
            private static int pos=0;
            private final String name;
            private CacheImmutale(String name)
            {
                  this.name=name;
            }
            public String getName()
            {
                  return name;
            }
            public static CacheImmutale valueOf(String name)
            {
                  //遍历已缓存的对象,
                  for (int 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 CacheImmutale(name);
                        //把pos设为1
                        pos=1;
                  }
                  else
                  {
                        //把新创建的对象缓存起来,pos加1
                        cache[pos++]=new CacheImmutale(name);
                  }
                  return cache[pos - 1];
            }
            public boolean equals(Object obj)
            {
                  if(this==obj)
                  {
                        return true;
                  }
                  if (obj !=null && obj.getClass()==CacheImmutale.class)
                  {
                        CacheImmutale ci=(CacheImmutale)obj;
                        return name.equals(ci.getName());
                  }
                  return false;
            }
            public int hashCode()
            {
                  return name.hashCode();
            }
        }
        public class CacheImmutaleTest
        {
            public static void main(String[] args)
            {
                  CacheImmutale c1=CacheImmutale.valueOf("hello");
                  CacheImmutale c2=CacheImmutale.valueOf("hello");
                  //下面代码将输出true
                  System.out.println(c1==c2);
            }
        }

当使用CacheImmutale类的valueOf方法来生成对象时,系统是否重新生成新的对象,取决于图6.3中被灰色覆盖的数组内是否已经存在该对象。如果该数组中已经缓存了该类的对象,系统将不会重新生成对象。

缓存实例的不可变类示意图
在这里插入图片描述
CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf方法来得到其对象,而且程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf方法来获取实例。

提示:
是否需要隐藏CacheImmutale类的构造器完全取决于系统需求。盲目乱用缓存也可能导致系统性能下降,缓存的对象会占用系统内存,如果某个对象只使用一次,重复使用的概率不大,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。

例如Java提供的java.lang.Integer类,它就采用了与CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf方法来创建Integer对象,则会缓存该方法创建的对象。下面程序示范了Integer类构造器和valueOf方法存在的差异。

        public class IntegerCacheTest
        {
            public static void main(String[] args)
            {
                  //生成新的Integer对象
                  Integer in1=new Integer(6);
                  //生成新的Integer对象,并缓存该对象
                  Integer in2=Integer.valueOf(6);
                  //直接从缓存中取出Ineger对象
                  Integer in3=Integer.valueOf(6);
                  //输出false
                  System.out.println(in1==in2);
                  //输出true
                  System.out.println(in2==in3);
                  //由于Integer只缓存-128~127之间的值
                  //因此200对应的Integer对象没有被缓存
                  Integer in4=Integer.valueOf(200);
                  Integer in5=Integer.valueOf(200);
                  System.out.println(in4==in5); //输出false
            }
        }

由于Integer只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象。

参考文献:《 疯狂java讲义》 李刚

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值