No.7 面向对象的陷阱

1. instanceof 运算符的陷阱

  • instanceof 运算符前面操作数的编译时类型必须是以下三种情况:①与后面的类相同②是后面的父类③是后面的子类。否则 无法通过编译(Incompatible conditional operand types String and Math)
  • 使用该运算符  从 编译、运行两个阶段来考虑
  • 强制转换机制
    • 编译阶段(需要有继承关系,否则编译错误)
      • 被转型变量的编译时类型与目标类相同
      • 被转型变量的编译时类型是目标类父类
      • 被转型变量的编译时类型是目标类子类,自动向上转型,无需强转
    • 运行阶段
      • 被转型变量所引用对象的实际类型 必须 是目标类型的 实例,或者是目标类的子类、实现类的实例,否则 CLassCastException
  • null可以作为任意对象的值,但是其并未引用一个真真的对象,故结果为false
    • 作用:instanceof 运算符 除了可以保证某个引用变量是特定类型的实例外,还可以保证该变量没有引用一个null。这样就可以将该引用变量转型为该类型,并调用该类型的方法,而不用担心会引发ClassCastException/NullPointerException异常
String str = null;
System.out.println(str instanceof String);// false

2. 构造器陷阱

  • 构造器不能加void,否则就会变成普通方法,如果没有其他正确的构造器,系统会默认提供一个无参构造器
  • 构造器并不会创建对象,只负责对象的初始化工作,对象所需的内存空间是new关键字申请出来的。不过一般情况下,new申请空间之后,都需要使用构造器对其进行初始化。以下两种情况创建对象无需使用构造器
    • 反序列化方式恢复Java对象
      • 通过反序列化方法得到的对象和原来对象具有完全相同的实例变量,但是是不同的两个变量
      • 那么单例类怎么办?一般不会使用反序列化破坏单例规则,如果真的想保证绝对单例,可以增加readResolve()方法,JVM反序列化时会调用该方法返回指定好的对象,从而保证不会新建对象
        package test1;
        
        import java.io.FileInputStream;
        import java.io.FileOutputStream;
        import java.io.IOException;
        import java.io.ObjectInputStream;
        import java.io.ObjectOutputStream;
        import java.io.ObjectStreamException;
        import java.io.Serializable;
        
        class Singleton implements Serializable{
            
            private static final long serialVersionUID = 1L;
            private String name;
            private static Singleton singleton;
            
            private Singleton(String name) {
                super();
                this.name = name;
            }
            public static Singleton getSingleton(String name) {
                if (singleton == null) {
                    singleton = new Singleton(name);
                }
                
                return singleton;
            }
            // readResolve() 方法
            private Object readResolve() throws ObjectStreamException{
                return singleton;
            }
            
            @Override
            public int hashCode() {
                final int prime = 31;
                int result = 1;
                result = prime * result + ((name == null) ? 0 : name.hashCode());
                return result;
            }
        
            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                Singleton other = (Singleton) obj;
                if (name == null) {
                    if (other.name != null)
                        return false;
                } else if (!name.equals(other.name))
                    return false;
                return true;
            }
            
        }
        public class SingletonTest {
            public static void main(String[] args) {
                Singleton singleton1 = Singleton.getSingleton("aa");
                Singleton singleton2 = null;
                
                ObjectOutputStream oos = null;
                ObjectInputStream ois = null;
                
                try {
                    oos = new ObjectOutputStream(new FileOutputStream("b.bin"));
                    ois = new ObjectInputStream(new FileInputStream("b.bin"));
                    
                    oos.writeObject(singleton1);
                    oos.flush();
                    
                    singleton2 = (Singleton) ois.readObject();
                    System.out.println(singleton1.equals(singleton2));
                    System.out.println(singleton1 == singleton2);
                }  catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } 
                finally{
                    if (oos != null) {
                        try {
                            oos.close();
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    if (ois != null) {
                        try {
                            ois.close();
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    
                }
            }
        }
        View Code
    • 使用clone方法复制Java对象
  • 无限递归的构造器
    • 尽量不要在定义实例变量时指定实例变量的值为当前类的实例
    • 尽量不要初始化块中创建当前类的实例
    • 尽量不要在构造器中调用本构造器创建Java对象(前两种本质上都将在构造器中执行,javap可看)

3. 持有当前类的实例

  • 有时候类的实例持有另一个实例是必要的,比如链表
  • 持有当前类是被允许的,只要初始化操作中没有  递归调用
  • 这种情形总是有风险的

4. 到底调用那个重载的方法

  • 调用方法时传入的实际参数可能被向上转型,通过这种向上转型可以使之符合被调用方法的实际需要
  • 重载解析过程(精确匹配原则
    • 阶段1:选取所有可获得并匹配调用的方法或者构造器。都选取出来
    • 阶段2:在阶段1选取的方法或者构造器总选取最精确匹配的那个(如果都可以的话,比如null,选取范围更小的那个。因为范围更小,方法就越精确)
      package test1;
      
      public class OverrideTest {
          public void info(Object object,double count) {//
              System.out.println("object参数为"+ object);
              System.out.println("count参数为"+ count);
          }
          public void info(Object[] objects,double count) {//
              System.out.println("objects参数为"+ objects);
              System.out.println("count参数为"+ count);
          }
          
          public static void main(String[] args) {
              OverrideTest overrideTest = new OverrideTest();
              overrideTest.info(null, 5);// 调用方法②,虽然①、②都可以,但是②的范围更小,所以更精确
          }
      }
      View Code

      如果JVM无法判断哪个更匹配,则会编译错误

      package test1;
      
      public class OverrideTest {
          public void info(Object object,int count) {//
              System.out.println("object参数为"+ object);
              System.out.println("count参数为"+ count);
          }
          public void info(Object[] objects,double count) {//
              System.out.println("objects参数为"+ objects);
              System.out.println("count参数为"+ count);
          }
          
          public static void main(String[] args) {
              OverrideTest overrideTest = new OverrideTest();
              overrideTest.info(null, 5);// 调用方法②,虽然①、②都可以,但是②的范围更小,所以更精确
          }
      }
      View Code

      The method info(Object, int) is ambiguous for the type OverrideTest

5. 方法重写的陷阱

  • 重写private方法
    • 子类无法访问父类中定义的private方法,因此也就没有重写一说了,哪怕子类和父类的方法一模一样

6. 非静态内部类的陷阱

  • 非静态内部类的构造器
    • 非静态内部类并没有无参数的构造器,它的构造器需要一个outer参数,符合其原则:非静态内部类必须寄生在外部类的实例中,不能单独存在
    • 系统在编译阶段总会为非静态内部类的构造器增加一个参数,非静态内部类的构造器的第一个形参类型总是外部类(outer)
    • 有时候程序上表面调用的是inner的无参数构造器创建实例,实际上,虚拟机会将this(代表当前默认的outer对象)作为实参传入构造器中。但是通过反射创建对象时调用的则是真真的无参构造器,因此会抛出  运行时异常
    • 调用非静态内部类的构造器时必须传入一个外部类对象作为参数(程序员显示调用构造器创建对象实例时,JVM底层会自动添加),否则程序将会引发运行时异常
  • 非静态内部类不能拥有静态成员
  • 非静态内部类的子类
    • 由于非静态内部类必须寄生在外部类的实例之中,程序创建非静态内部类的实例及派生非静态内部类的子类时都必须特别小心
    • 推荐多使用静态内部类。对静态内部类而言,外部类相当于它的一个包,因此其用法就简单的多了

7.static关键字

  • 静态方法属于类
    • 静态方法属于类,当通过变量来调用静态方法时,底层实际上会委托  声明的类  来作为主调来调用方法,因此调用的是声明类的静态方法
    • 静态内部类不能访问外部类的非静态成员

8. native方法

  • native方法做不到跨平台
  • why:Java程序中该方法只有方法签名,没有方法体;具体实现一般是通过C完成,编译.cpp文件依赖于当前的编译平台,所以。。。
  • 虽然Java语言是跨平台的,但其native方法还是要依赖于具体的平台,尤其是jdk提供的方法,包含了大量的native方法。使用时要稍加注意
PS:不足之处,欢迎指正、交流
原文地址:https://www.cnblogs.com/fang--/p/6271040.html