• Java重要知识点


    1.Java中除了static方法和final方法之外,其它所有的方法都是动态绑定,如同C++的虚函数,但是我们不需要显示的声明。

      private方法本质上属于final方法(因此不能被子类访问)。

      构造函数本质上属于static方法,只不过该static声明是隐式的。

      final方法会使编译器生成更有效的代码,这也是为什么说声明为final方法能在一定程度上提高性能(效果不明显)。

      如果某个方法是静态的,它的行为就不具有多态性。

      在父类构造函数内部调用具有多态行为的函数将导致无法预测的结果,因为此时子类对象还没初始化,此时调用子类方法不会得到我们想要的结果。

      只有非private方法才可以被覆盖,但覆盖private方法对子类来说是一个新的方法而非重载方法。因此,在子类中,新方法名最好不要与基类的private方法采取同一名字。

      Java类中属性域的访问操作都由编译器解析,因此不是多态的。父类和子类的同名属性都会分配不同的存储空间。为了得到父类的属性field,必须显式地指明super.field。

    2.is-a关系和is-like-a关系

      is-a关系属于纯继承,即只有在基类中已经建立的方法才可以在子类中被覆盖。基类和子类有着完全相同的接口,这样向上转型时永远不需要知道正在处理的对象的确切类型,这通过多态来实现。

      is-like-a关系:子类扩展了基类接口。它有着相同的基本接口,但是他还具有由额外方法实现的其他特性。缺点就是子类中接口的扩展部分不能被基类访问,因此一旦向上转型,就不能调用那些新方法。

      has-a关系:某个类包含另一个类或接口的引用。

    3.Java中构造函数的调用顺序

        a.在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制0。

        b.调用基类构造函数。

            c.按声明顺序调用成员的初始化方法。

        d.最后调用子类的构造函数。

      由于存在表态数据,实际过程为

        b.继承体系的所有静态成员初始化(先父类,后子类)

        c.父类初始化完成(普通成员的初始化-->构造函数的调用)

        d.子类初始化(普通成员-->构造函数)

    4.运行时类型信息(RTTI + 反射)

      运行时类型信息使得你可以在程序运行时发现和使用类型信息。

      Java是如何让我们在运行时识别对象和类的信息的,主要有3种方式

        “传统的”RTTI,它假定我们在编译时已经知道了所有的类型,比如Shape s = (Shape)s1;

        “反射”机制,它运行我们在运行时发现和使用类的信息,即使用Class.forName()

        关键字instanceof,它返回一个bool值,它保持了类型的概念,它指的是“你是这个类吗?或者你是这个类的派生类吗?”。而如果用==或equals比较实际的Class对象,就没有考虑继承—它或者是这个确切的类型,或者不是。

      RTTI:Run-Time Type Identification,即运行时类型识别,是指在运行时识别一个对象的类型,其对应的类是Class对象,每个java里面的类都对应一个Class对象(在编写并且编译后),这个对象被保存在这个类的同名class文件里。  

      要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的,这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。Java送Class对象来执行其RTTI,使用类加载器的子系统实现。

      无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用,获取方式有三种:

        a.如果你没有持有该类型的对象,则Class.forName()就是实现此功能的便捷途,因为它不需要对象信息。

        b.如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass()方法来获取Class引用了,它将返回表示该对象的实际类型的Class引用。

        c.使用类字面常量。比如这样:String.class;来引用。

        这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中),并且它根除了对forName方法的引用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。

        注意:

        当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象,初始化被延迟到了对静态方法(构造器隐式的是静态的)或者非final静态域(注意final静态域不会触发初始化操作)进行首次引用时才执行。

        而使用Class.forName时会自动的初始化。

      为了使用类而做的准备工作实际包含三个步骤:

        - 加载:由类加载器执行。查找字节码,并从这些字节码中创建一个Class对象

        - 链接:验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。

        - 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

      如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,也就是在编译时,编译器必须知道所有要通过RTTI来处理的类。如果要突破这个限制就需要使用反射机制。

      反射的原理:

    Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了FieldMethod以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get()/set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

       反射与RTTI的区别

    当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样),在用它做其他事情之前必须先加载那个类的Class对象,因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。所以RTTI与反射之间真正的区别只在于:对RTTI来说,编译器在编译时打开和检查.class文件(也就是可以用普通方法调用对象的所有方法);而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

     

     5.代理模式与Java中的动态代理

    代理模式
      在任何时刻,只要你想要将额外的操作从“实际”对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用(设计模式的关键是封装修改)。例如,如果你希望跟踪对某个类中方法的调用,或者希望度量这些调用的开销,那么你应该怎样做呢?这些代码肯定是你不希望将其合并到应用中的代码,因此代理使得你可以很容易地添加或移除它们。

    动态代理
      Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。

    测试代码

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    interface Interface {
        void doSomething();
        void somethingElse(String arg);
    }
    
    class RealObject implements Interface {
        @Override
        public void doSomething() {
            System.out.println("[RealObject]doSomething.");
        }
    
        @Override
        public void somethingElse(String arg) {
            System.out.println("[RealObject]somethingElse:" + arg);
        }
    }
    
    class DynamicProxyHandler implements InvocationHandler {
    
        private Object mProxy;
    
        public DynamicProxyHandler(Object proxy) {
            mProxy = proxy;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            System.out.println("proxy class: " + proxy.getClass());
            System.out.println("method: " + method + ". args: " + args);
            System.out.println();
            if (args != null) {
                for (Object arg : args)
                    System.out.println(" " + arg);
            }
            return method.invoke(this.mProxy, args);
        }
    }
    
    class SimpleDynamicProxy {
        public static void consumer(Interface iface) {
            iface.doSomething();
            iface.somethingElse("哈哈");
        }
    
        public static void main(String[] args) {
            RealObject real = new RealObject();
            consumer(real);
            System.out.println("-- insert a proxy and call again --");
            Interface proxy = (Interface) Proxy.newProxyInstance(
                    Interface.class.getClassLoader(),
                    new Class[] { Interface.class }, new DynamicProxyHandler(real));
            consumer(proxy);
        }
    }

      结果

    [RealObject]doSomething.
    [RealObject]somethingElse:哈哈
    -- insert a proxy and call again --
    proxy class: class $Proxy0
    method: public abstract void Interface.doSomething(). args: null
    
    [RealObject]doSomething.
    proxy class: class $Proxy0
    method: public abstract void Interface.somethingElse(java.lang.String). args: [Ljava.lang.Object;@6adcc4e2
    
     哈哈
    [RealObject]somethingElse:哈哈

     

    6.访问控制权限

    Java访问权限修饰词:public、protected、包访问权限(默认访问权限,有时也称friendly)和private。

    包访问权限:当前包中的所有其他类对那个成员具有访问权限,但对于这个包之外的所有类,这个成员却是private。

    protected:继承访问权限。protected使基类某些成员访问权限赋予派生类。protected也提供包访问权限,即相同包内的其他类都可以访问protected元素。protected指明对类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。

    7.组合和继承之间的选择

      组合和继承都允许在新的类中放置子对象,组合是显式的这样做,而继承则是隐式的做。

      组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。但有时,允许类的用户直接访问新类中的组合成分是极具意义的,即将成员对象声明为public。如果成员对象自身都隐藏了具体实现,那么这种做法是安全的。当用户能够了解到你正在组装一组部件时,会使得端口更加易于理解。比如Car对象可由public的Engine对象、Wheel对象、Window对象和Door对象组合。但务必要记得这仅仅是一个特例,一般情况下应该使域成为private。

      在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。稍微思考一下就会发现,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。

      “is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系则是用组合来表达的。

      到底是该用组合还是继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型,需要的话就用继承,不需要的话就用组合方式。

    8.final关键字

      当final修饰的是基本数据类型时,它指的是数值恒定不变(就是编译期常量,如果是static final修饰,则强调只有一份),而对对象引用而不是基本类型运用final时,其含义会有一点令人迷惑,因为用于对象引用时,final使引用恒定不变,一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。然而,对象其自身却是可以被修改的,Java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果),这一限制同样适用数组,它也是对象。

    使用final方法真的可以提高程序效率吗?
      将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。

      虚拟机(特别是hotspot技术)能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

      类内所有private 方法都自动成为final。由于我们不能访问一个private 方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。

    记住:只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

     9.内部类

    为什么需要内部类? 解决了多继承的问题,继承具体或抽象类。

    一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。

    内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

    如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效的实现了“多重继承”。也就是说,内部类允许继承多个非接口类型。

    考虑这样一种情形:如果必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择:使用单一类或者使用内部类。但如果拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承。

    使用内部类,还可以获得其他一些特性:
    - 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
    - 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口或继承同一个类。
    - 创建内部类对象的时刻并不依赖于外围类对象的创建。
    - 内部类并没有令人迷惑的is-a关系,它就是一个独立的实体。

    10.String类型

    String类型是一个不可变的类型,任何对String对象改变的操作都会生成一个新的实例。

    用于String的“+”与“+=”是Java中仅有的两个重载过的操作符,而Java并不允许程序员重载任何操作符。

    考虑到效率因素,编译器会对String的多次+操作进行优化,优化使用StringBuilder操作。但是在存在一个循环执行+时,手动使用StringBuilder代替String。考虑下面的代码,在每一次For循环之内,都将创建一个StringBuilder对象。

        public String implicit(String[] fields) {
            String result = "";
            for(int i = 0; i < fields.length; i++)
                result += fields[i];
            return result;
        }

      此种情况下,请使用下面的方法替代

        public String explicit(String[] fields) {
            StringBuilder result = new StringBuilder();
            for(int i = 0; i < fields.length; i++)
                result.append(fields[i]);
            return result.toString();
        }

      当你为一个类重写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果

    11.序列化控制

      当我们对序列化进行控制时,可能某个特定子对象不想让Java序列化机制自动保存与恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常会面临这种情况。即使对象中的这些信息是private属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它。有两种办法可以防止对象的敏感部分被序列化:

      实现Externalizable代替实现Serializable接口来对序列化过程进行控制,Externalizable继承了Serializable接口,同时增添了两个方法:writeExternal()readExternal()

    两者在反序列化时的区别:
      - 对Serializable对象反序列化时,由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
      - 对Externalizable对象反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
      - Externalizable的替代方法:如果不是特别坚持实现Externalizable接口,那么还有另一种方法。我们可以实现Serializable接口,并添加writeObject()readObject()的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。

      这些方法必须含有下列准确的签名:

      private void writeObject(ObjectOutputStream stream)  throws IOException;
      private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException

      - 可以用transient关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据—我自己会处理的”。由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。

     12.泛型

    无论所时,只要可以就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。

     1 class Order<T> {
     2     public final T tFinal;
     3     public Order(T v) {
     4         tFinal = v;
     5     }
     6     public T getT() {
     7         return tFinal;
     8     }
     9     
    10     public static <T> void staticFun(Order o) { // 这里必须使用泛型方法
    11         T t = (T) o.getT();
    12         System.out.println(t.getClass());
    13     }
    14     
    15     public void nonstaticFun(Order o) {
    16         T t = (T) o.getT();
    17         System.out.println(t.getClass());
    18     }
    19     
    20     @Override
    21     public String toString() {
    22         return tFinal.getClass().getCanonicalName();
    23     }
    24 }
    View Code

    参考资料:

    《Java编程思想》

    http://www.cnblogs.com/lanxuezaipiao/p/4153070.html

    代码养活自己
  • 相关阅读:
    oracle存储过程+游标处理select数据
    C/C++中各种类型int、long、double、char表示范围(最大最小值)
    算法如功夫——C++ 用递归函数计算n的阶乘n!
    北邮iptv用WindowsMediaplayer打不开的解决的方法
    【hoj】2651 pie 二分查找
    抽象工厂模式
    Java NIO与IO的差别和比較
    spark未来的发展方向
    Android学习笔记(四十):Preference的使用
    android 原生应用、Web应用、混合应用优缺点分析
  • 原文地址:https://www.cnblogs.com/diysoul/p/5042176.html
Copyright © 2020-2023  润新知