美文网首页
温故而知新-Java 提升篇

温故而知新-Java 提升篇

作者: HansenGuan | 来源:发表于2017-08-15 17:49 被阅读0次

    Overriding(重写) & Overloading(重载)

    • Overriding - same method names with same arguments and same return types associated in a class and its subclass.(一个类和它的子类相同方法名、参数和返回类型)
    • Overloading - same method name with different arguments, may or may not be same return type written in the same class itself.(同一个类中同方法名不同参数)

    强引用、弱引用,[链接1链接2]

    • 如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    • 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

    finalize()函数是在JVM回收内存时执行的,但JVM并不保证在回收内存时一定会调用finalize()。


    final, finally, finalize

    • 如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重载
    • 在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
    • Java 技术允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

    深拷贝(deep clone),浅拷贝(shallow clone)

    • 克隆就是复制一个对象的复本.但一个对象中可能有基本数据类型,如:int,long,float 等,也同时含有非基本数据类型如(数组,集合等)
    • 被克隆得到的对象基本类型的值修改了,原对象的值不会改变.这种适合shadow clone(浅克隆).
    • 如果你要改变一个非基本类型的值时,原对象的值却改变了,比如一个数组,内存中只copy他的地址,而这个地址指向的值并没有 copy,当clone时,两个地址指向了一个值,这样一旦这个值改变了,原来的值当然也变了,因为他们共用一个值.,这就必须得用深克隆(deep clone)

    结论:

    1. 浅克隆:基本类型是可以被克隆的,但引用类型只是copy地址,并没有copy这个地址指向的对象的值,这使得两个地址指向同一值,修改其中一个,当然另一个也就变了.浅克隆只适合克隆基本类型,对于引用类型就不能实现克隆了.
    • 通过implements Cloneable 重写 clone方法实现

    1. 可以用序列化与反序列化实现深克隆(deep copy)
    • 通过 implements Serializable 实现

    当克隆的对象只有基本类型,不含引用类型时,可以用浅克隆实现.
    当克隆的对象含有引用类型时,必须使用深克隆实现.

    - java提供一种叫浅拷贝(shallow copy)的默认方式实现clone,创建好对象的副本后然后通过赋值拷贝内容,意味着如果你的类包含引用类型,那么原始对象和克隆都将指向相同的引用内容。发生在可变的字段上任何改变将反应到他们所引用的共同内容上。为了避免这种情况,需要对引用的内容进行深度克隆。
    

    comparable接口与comparator

    • 实现了 Camparable 接口表明这个类的对象之间是可以相互比较的。意味着这个类对象组成的集合就可以使用 Sort 方法排序了。
    • Comparator 的作用有两个:
      • 没有实现 Comparable 接口,可以通过 Comparator 来实现比较算法进行排序
      • 为了使用不同的排序标准做准备

    结论:
    “集合框架” 中有两种比较接口: Comparable 接口和 Comparator 接口:

    • Comparable 是通用的接口,用户可以实现它来完成自己特定的比较
    • Comparator 可以看成一种算法的实现,在需要容器集合实现比较功能的时候,来指定这个比较器,这可以看成一种设计模式,将算法和数据分离。

    hashcode & equals [链接]

    • 覆写equals方法
    • 覆写hashcode
    • 两个对象如果equals那么这两个对象的hashcode一定相等,如果两个对象的hashcode相等那么这两个对象是否一定equals?
      • 这要看这两个对象有没有重写Object的hashCode方法和equals方法。如果没有重写,是按Object默认的方式去处理。
      • '==' 比较地址,equal 自定义
    • hashcode中为什么要使用 31 这个数?

      • 31 是一个素数(质数),如:我们选择素数3来做系数,那么3*n只能被3和n或者1来整除,我们可以很容易的通过3n来计算出这个n来。
      • 任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多,对左移现在很多虚拟机里面都有做相关优化,并且31只占用5bits!

    可变类与不可变类的区别

    • 当创建了这个类的实例后,就不允许修改它的属性值。在JDK的基本类库中,所有基本类型的包装类,如Integer和Long类,都是不可变类,java.lang.String也是不可变类。
    • 如何创建一个不可变实例类:
      • 1. 所有成员都是private
      • 2. 不提供对成员的改变方法,例如:setXXXX
      • 3. 确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
      • 4. 如果某一个类成员不是原始变量(primitive)或者不可变类,必须通过在成员初始化(in)或者get方法(out)时通过深度clone方法,来确保类的不可变。

    字符串常量池 ,深入解析String#intern

    • JAVA 语言中8中基本类型和一种比较特殊的类型String,为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。
    • 8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
      • 直接使用双引号声明出来的String对象会直接存储在常量池中。
      • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
      • String#intern方法是一个 native 的方法:如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回

    Q: String s = new String("abc")这个语句创建了几个对象的题目?(考察字符串对象的常量池)
    A: 上述的语句中是创建了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。

    • 在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m
    • jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域

    Java 泛型 [泛型详解]

    • 泛型基础
      • 泛型类
      • 泛型方法
      • 边界符
        • 类似于T extends Comparable<T>这样的声明,告诉编译器类型参数T代表的都是实现了Comparable接口的类
      • 通配符: ?
    • PECS原则[”Producer Extends, Consumer Super”]

      • ? super T 与 ? extends T 的区别
      • 生产者(Producer)使用extends,消费者(Consumer)使用super。
      • “Producer Extends” - 如果你需要一个只读List,用它来produce T,那么使用? extends T
      • “Consumer Super” - 如果你需要一个只写List,用它来consume T,那么使用? super T
      • 如果需要同时读取以及写入,那么我们就不能使用通配符了。
    • 类型擦除

      • 类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就不知道泛型所代表的具体类型。
      • Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。

    String, StringBuffer, StringBuilder的区别

    • String:字符串常量,字符串长度不可变,不可变类
    • StringBuffer:字符串变量(Synchronized,即线程安全)
      • 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用
      • 字符串对象经常改变的情况
    • StringBuilder:字符串变量(非线程安全)

    1. 如果要操作少量的数据,用String ;单线程操作大量数据,用StringBuilder ;多线程操作大量数据,用StringBuffer。
    2.StringBuffer 或 StringBuilder 时应尽可能指定它们的容量


    类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据;构造函数,字段的执行顺序 [链接]

    • 实例化顺序 / 执行顺序
      • 非继承
        • (静态变量、静态初始化块)>(变量、初始化块)> 构造器
      • 继承
        • 父类(静态变量、静态初始化块)> 子类(静态变量、静态初始化块) > 子类main方法 > 父类(变量、初始化块) > 父类--构造器 > 子类(变量、初始化块) > 子类--构造器
      • 静态变量和静态初始化块的声明顺序决定了初始化的顺序

    本地方法栈和虚拟机栈 、堆

    • 虚拟机栈
      • JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。
    • 本地方法栈
      • JVM规范为了允许native代码可以调用Java代码,以及允许Java代码调用native方法,还规定每个Java线程拥有自己的独立的native方法栈。
      • JVM里的“堆”(heap)特指用于存放Java对象的内存区域。根据这个定义,Java对象全部都在堆上。

    注意事项

    1. 虚拟机栈、本地方法栈JVM规范所规定的概念上的东西,并不是说具体的JVM实现真的要给每个Java线程开两个独立的栈。可能只使用一个栈,融合以上两个栈的概念
    2. 堆不是数据结构意义上的堆(Heap,一种有序的树),而是动态分配意义上的堆---用于管理动态生命周期的内存区域
    3. JVM堆被同一个JVM实例中的所有线程共享,通常由自动内存管理机制管理(“垃圾回收”,GC,garbage collection)


    Java内存模型链接

    • Java内存模型,往往是指Java程序在运行时内存的模型


    • 运行时内存模型,分为线程私有和共享数据区两大类


      • 线程私有
        1. 程序计数器:记录正在执行的虚拟机指令码的地址
        2. 虚拟机栈:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧
        3. 本地方法区:虚拟机的Native方法执行的内存区
      • 共享数据区
        1. Java堆:对象分配内存的区域
        2. 方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据
        • 在方法区内有一个常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分

    为什么函数调用要用栈实现?【链接

    • 函数的调用有完美的嵌套关系——调用者的生命期总是长于被调用者的生命期,并且后者在前者的之内
    • 被调用者的局部信息所占空间的分配总是后于调用者的(后入),而其释放则总是先于调用者的(先出),所以正好可以满足栈的LIFO顺序
    • 函数调用的局部状态之所以用栈来记录是因为这些数据的存活时间满足“后入先出”(LIFO)顺序,而栈的基本操作正好就是支持这种顺序的访问。

    反射和动态代理

    • Java 反射机制可以在运行时期检查 Java 类的信息
    • 类的信息 包括:
      1. Class 对象
        Class.forName()必须提供一个类的全名
        MyObject.class前提知道类名
      2. 类名
        getName()获取类的全限定类名(包含包名)
        getSimpleName()获取类名(不包含包名)
      3. 修饰符
        getModifiers()获取类的修饰符,使用java.lang.reflect.Modifier类中的方法来检查修饰符的类型
        Modifier.isPrivate(int modifiers);
      4. 包信息
        getPackage()获取包信息
      5. 父类
        getSuperclass()访问类的父类
      6. 实现的接口
        getInterfaces()获取类所实现的接口集合(只有实现了接口才返回)
      7. 构造器
        getConstructors()获取类的构造方法
      8. 方法
        getMethods()获取类的所有方法
      9. 变量
        getFields()获取类的所有成员变量
      10. 注解
        getAnnotations()获取类的所有注解
    • 代理(参考链接


      代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等
      为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。
      按照代理的创建时期,代理类可以分为两种:
    • 静态代理
      在程序运行前代理类的.class文件就已经存在了
      • 优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。
      • 缺点:
        1. 一个接口只服务于一种类型的对象,代理类增多时,会造成重复代码
        2. 如果接口增加一个方法所有的实现类和代理类都要实现该方法,增加了代码的维护负责度
    • 动态代理(JDK)-- 通过接口
      在程序运行时运用反射机制动态创建而成
      • 与动态代理相关的API:
        • java.lang.reflect.Proxy
          • 这是 Java 动态代理机制生成的所有动态代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
        • java.lang.reflect.InvocationHandler
          • 这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
          • 每次生成动态代理类对象时都要指定一个对应的调用处理器对象。
        • java.lang.ClassLoader
          • Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。
          • 每次生成动态代理类对象时都需要指定一个类装载器对象
      • 动态代理实现步骤
        1. 实现InvocationHandler接口创建自己的调用处理器
        2. 给Proxy类提供ClassLoader和代理接口类型数组创建动态代理类
        3. 以调用处理器类型为参数,利用反射机制得到动态代理类的构造函数
        4. 以调用处理器对象为参数,利用动态代理类的构造函数创建动态代理类对象
    > Proxy类的静态方法`newProxyInstance`对上面具体步骤的后三步做了封装,简化了动态代理对象的获取过程
      - 动态代理的优点与不足
        - 优点:动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)
        - 缺点:仅支持 interface 代理,__无法实现对 class 的动态代理__(原因是多继承在 Java 中本质上就行不通)
    
    • 动态代理(Cglib) -- 通过类继承
      • CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。
      • 创建类A的动态代理类的模式
        1. 查找A上的所有非final 的public类型的方法定义;
        2. 将这些方法的定义转换成字节码;
        3. 将组成的字节码转换成相应的代理的class对象;
        4. 实现 MethodInterceptor 接口,用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)

    总结:
    1. 为了解决使用静态代理会造成系统结构臃肿的问题,在运行状态中,需要代理的地方,根据Subject 和RealSubject,动态地创建一个Proxy,用完之后,就会销毁,这样就可以避免了Proxy 角色的class在系统中冗杂的问题了。(动态代理的优势)


    2.为了构造出具有通用性和简单性的代理类,可以将所有的触发真实角色动作交给一个触发的管理器,让这个管理器统一地管理触发。这种管理器就是Invocation Handler。(Invocation Handler角色的由来)


    3.动态代理工作的基本模式就是将自己的方法功能的实现交给 InvocationHandler角色,外界对Proxy角色中的每一个方法的调用,Proxy角色都会交给InvocationHandler来处理,而InvocationHandler则调用具体对象角色的方法。




    4.约定Proxy 和RealSubject可以实现相同的功能,有两种方式:
    • 定义一个功能接口,然后让Proxy 和RealSubject来实现这个接口。(JDK创建动态代理用的这种思路)
    • 通过继承。因为如果Proxy 继承自RealSubject,这样Proxy则拥有了RealSubject的功能,Proxy还可以通过重写RealSubject中的方法,来实现多态。(cglib使用这种思路)

    5.两种方式的比较:

    • JDK:接口特点可代理多个接口,无法代理未实现接口的类
    • Cglib:继承特点可直接代理类无需实现接口,无法一次代理多个类

    反射中Class.forName 和 ClassLoader的区别

    • 类装载过程


      • 加载
        通过累的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lang.class对象
      • 链接
        • 验证
          检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证)
        • 准备
          给类的静态变量分配并初始化存储空间;
        • 解析
          将常量池中的符号引用转成直接引用;
      • 初始化
        激活类的静态变量的初始化Java代码和静态Java代码块,并初始化程序员设置的变量值。
      • 使用
      • 卸载
    • Java对类的使用分为两种方式:主动使用(new)和被动使用(类装载)
    • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
    • Class.forName()前者除了将类的.class文件加载到jvm中之外,默认还会对类进行解释,执行类中的static块。

    • classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

    • Class.forName(className)默认是需要初始化的
      一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。
    • ClassLoader.loadClass(className)内部实际调用的方法是 ClassLoader.loadClass(className,false)
      第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接
      不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

    数据库链接为什么使用Class.forName(className)?
    JDBC Driver源码

    static{
     try{
       java.sql.DriverManager.registerDriver(new Driver());
     } catch(SQLException e){
       throw new RuntimeException("Can't register driver!");
     }
    }
    

    因此使用Class.forName(classname)才能在反射回去类的时候执行static块


    Iterator设计思想

    如何实现两种容器的可替换性(底层容器的实现随意改变不影响用户使用)?

    统一接口、面向接口编程

    以ArrayList和LinkedList为例

    ArrayList的继承图:



    LinkedList的继承图:


    ArrayList和LinkedList都实现了Collection接口、Iterator接口

     //通过实现统一Collection接口,面向接口编程,实现底层容器切换不影响使用,达到对用户透明的效果
     Collection<String> collection = new ArrayList<String>();
     //Collection<String> collection = new LinkedList<String>();
     collection.add("hello");
     collection.add("java");
     collection.remove("hello");
    

    Iterator 基于以上思想实现 Collection 所有实现类的统一遍历方式,不需关心具体实现数据结构(数组,链表),具体的遍历方式由容器自己根据自身特点实现

    1. 实现Iterable接口的类都可以使用“foreach”操作

    /** Implementing this interface allows an object to be the target of
    *  the "foreach" statement.
    * @since 1.5
    */
    public interface Iterable<T> {
      /**
       * Returns an iterator over a set of elements of type T.
       *
       * @return an Iterator.
       */
      Iterator<T> iterator();
    
      ····
    }
    

    2. 统一 Iterator 接口

    public interface Iterator<E> {
      // 是否还有元素
      boolean hasNext();
      // 下一个元素
      E next();
      // 将迭代器返回的元素删除
      void remove();
    
      ···
    }
    

    3. ArrayList的Iterator的具体实现


    通过继承 Iterable 接口,定义 Iterator 规范 迭代方法,Collection 的所有实现类实现自己的 Iterator 接口,实现统一的、对用户透明的迭代方法

    使用示例:

    Iterator<String> iterator = collection.iterator();
    while (iterator.hasNext()) {
         System. out.println(iterator.next());
    }
    

    AbstractList 抽象类中 modCount 变量分析

    总结:

    这个数统计list 发生结构性改变的次数
    这个变量被 iterator 和 listIterator 方法返回的 iterator 实现使用,如果这个变量发生非预期的改变,iterator 将在 next , remove , previous , set 或者 add操作时抛出 ConcurrentModificationException 异常,提供了一个快速响应失败的机制(fail-fast)而不是在遍历时进行不确定的行为。

    private class Itr implements Iterator<E>{
        int expectedModCount = modCount;  //遍历前获取当前list 的 修改数
        ···
        //实现Iterator接口定义的方法
        ···
        final void checkForComodification() {
            //当Iterator中的存储的 修改数 与 list 中的修改数不一致时,说明遍历时,list结构发生了改变
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    
    }
    

    当多线程操作同一容器的时候,一个线程通过Iterator遍历容器的同时,另一个线程修改了改容器的内容(add、remove等操作),Iterator及时抛出异常,防止发生 不确定性 的 行为。


    相关文章

      网友评论

          本文标题:温故而知新-Java 提升篇

          本文链接:https://www.haomeiwen.com/subject/ozsvrxtx.html