Java面向对象(包装类、toString、==和equals)

124 阅读14分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

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);
            }
        }