java反射机制剖析(一)—简介

    由之前动态代理的学习再次接触到反射这个知识点,第二次接触了所以做了一些稍微深入的了解。那么,对于反射这部分的内容我打算分三篇博客来总结。本篇博客先对反射做一个大概的了解,包括反射有关的RTTI、定义的理解以及涉及到的其他知识的简介。

回顾

    java之前我接触反射这个知识,是在大话设计中的抽象工厂模式里,通过反射+配置文件来优化抽象工厂提高其应对需求变更的灵活性。当时对于反射的认知仅仅是它是一种技术,一种实例化对象的技术,一种实例化对象不依赖于写死的代码的技术。简单的说就是,它是一种可以摆脱用NEW去实例化对象,显然它应付与对象变换的能力是强大的。
    其实,反射不论在什么语言里都是一样的,只不过实现的手段不一样而已。之前对代理模式的深入学习过程中又遇到了反射,所以决定这次要一探究竟。

定义

    Java可以在运行时加载、探知、使用编译期间完全未知的class。再简单一点说就是java可以在运行时获得任一一个类的信息、构建类的Class对象(强调:不是该类的对象,是后面提高的类类型)、生成类实例、调用methods。这里类的信息包括它的基类、所实现的接口、方法等。
    个人觉得这里比较难理解的是“编译期间完全未知”。所以,特别解释一下。如下的代码。首先,这个Note类是不存在的,也就是说这段代码有错。分别执行两个方法就可以看出分别了,其实方法2编译时就无法通过,提示类是不能识别的类型,因为本来就不存在这个类。而方法1则时可以通过编译,执行时能打印“方法执行中”。但是实例化时会报空指针的错误。这两种不同时刻产生的错误就说明了“编译期间完全未知”这个说法。
  1. public class Client {  
  2.      public static void main(String[] args)   
  3.              throws InstantiationException, IllegalAccessException, ClassNotFoundException{   
  4.                        System.out.println("方法执行中!"); //打印说明进入该方法   
  5.                        //方法1:通过反射实例化类Note      
  6.         Class c=Class.forName("com.zjjReflect.Note");  
  7.            Object o=c.newInstance();      
  8.            //方法2:直接实例化类Note   
  9.            //Object o=new Note();   
  10.          }  
  11. }  
public class Client {
     public static void main(String[] args) 
    		 throws InstantiationException, IllegalAccessException, ClassNotFoundException{	
                       System.out.println("方法执行中!"); //打印说明进入该方法
                       //方法1:通过反射实例化类Note	
		Class c=Class.forName("com.zjjReflect.Note");
	       Object o=c.newInstance();	
	       //方法2:直接实例化类Note
	       //Object o=new Note();
	     }
}

RTTI

     RTTI是Run-Time  Type Information 的缩写,意思是运行时类型信息。,RTTI提供了运行时确定对象类型的方法。但是,RTTI并不是一种技术,应该是一种概念。因为不同的语言对RTTI的实现形式是不一样的。简单的说就是在程序运行时去获得一个对象所对应的类的信息。这么说有点模糊,还是结合RTTI在某种语言里的实现来理解好一些。Java 中就是主要有:类型转换、类类型Class、instanceof体现了RTTI。

类类型java.lang.Class

    Class是所有类和接口的抽象,比如类的名字、类实现的接口、方法、属性等等。你可以通过某个对象的Class对象来获取类的信息。这里不做详细介绍,后面有单独的博客补充。

Class loader

    类加载的过程就是将.class加载到内存中。这里为什么要提到classloader呢?因为反射的过程使用到了Classloader,并且不同的类需要相对应的Classloader来加载。也就是说Classloader是和类是配对工作的,然后反射的特点却是在运行时才会知道类的信息
    所以我们也要对这部分的内容作个了解。也是为了能更好、更深刻的对反射理解。Java的Classloader有四种分别为:
         bootstrap classloader :引导(也称为原始)类加载器。                 
         extension classloader :扩展类加载器。
         Application ClassLoader:应用程序类加载器。
         User Defined ClassLoader:自定义类加载器。
    总结:在程序运行时通过类类型Class获得目标类的信息,然后在类信息的基础上使用相对应的类加载器加载到内存中,再然后对这个类中进行实例化,方法调用等的使用的整个过程。就是反射的详细的说法!!
 
 
上面简要的提了一下java反射机制中涉及到的一些相关知识,那么ClassLoader就是其中之一。紧接着就详细的对ClassLoader做一个相对深入的了解。作为了解需要知道的是,其实类类型(Class Class)是和ClassLoader分不开的,因为ClassLoader需要的信息是由它提供的。类类型将在下一篇博客介绍。

简介

    ClassLoader是负责加载类的对象,作用是根据Jvm请求提供的类信息,将请求的类加载的内存中或者说加载到Jvm中。另外,每一个类的Class对象(注意Class是类类型)都持有一个对应的ClassLoader的引用。可以通过Class对象的getClassLoader()方法得到。类和它的ClassLoader是对应的,所以类只能通过它对应的ClassLoader加载。
    注意数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类加载器由 Class.getClassLoader() 返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。

分类

    JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。
    Bootstrap ClassLoader:是用C++编写的,是JVM的内置加载器,它的名字是null。它用来加载核心类库,即在lib下的类库。做个实验,首先,String类肯定是java的核心类,那我们就以它为例来看看:    
  1. public static void main(String[] args){  
  2.         String a="x";  
  3.         System.out.println(a.getClass().getClassLoader());  
  4.     }  
public static void main(String[] args){
		String a="x";
		System.out.println(a.getClass().getClassLoader());
	}
           我们通过代码来获得String加载对应的ClassLoader的名字输出的结果为NULL。
    Extension ClassLoader:加载lib/ext下的类库。
    App ClassLoader:加载Classpath里的类库。

层次关系

    之前我们说过,每一个Class对象都会持有一个对应的ClassLoader的引用。每一个ClassLoader对象也会持有一个Parent ClassLoader的引用。这里需要特别注意的是:这里所指的的Parent ClassLoader不是我们熟悉的继承关系,不是父类!!首先,我们要知道这里说的是ClassLoader对象,也就是说这的Parent ClassLoader其实是一个对象的引用。下面看一张图,了解一下ClassLoader对象之间的层次关系:
     java反射机制剖析(一)—简介     


      我们这里可以在做一个实验,来体会一下这个层次关系。代码如下:
         
  1. public static void main(String[] args){       
  2.         ClassLoader c =TestClassLoader.class.getClassLoader();  
  3.         do {  
  4.             System.out.println(c.getClass().getName());  
  5.             c=c.getParent();  
  6.         }while(c!=null);          
  7.     }  
  8. }  
public static void main(String[] args){		
		ClassLoader c =TestClassLoader.class.getClassLoader();
		do {
			System.out.println(c.getClass().getName());
			c=c.getParent();
		}while(c!=null);		
	}
}
输出的结果为:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

双亲加载机制

    层次关系中我们了解到了很重要的一点:加载器对象之间的引用关系。被引用的对象称之为引用对象的父加载器,可以通过getParent()方法得到。那么双亲加载机制就是基于这种引用的层次关系。即:当一个ClassLoader接到请求时,它不是直接加载对应的类,而是询问它引用的ClassLoader是否能够加载,而这个父ClassLoader则会询问自己的引用的ClassLoader是否加载了该类。只有当所有的父ClassLoader都没有加载该类时,这个最初的ClassLoader才自己去加载申请的类。
    很绕啊,文字说不清楚还是上图吧!

java反射机制剖析(一)—简介

    双亲加载机制可以一定程度上保证安全性,因为只要顶层ClassLoader能加载的东西就一定不会让下层的ClassLoader有机会加载。也就保证了有些自定义的带有破坏性的类不会被加载到Jvm核心中。
    结语:ClassLoader相对难理解的地方一个是,对象将的层次关系和父加载器。另一个是双亲加载机制。这里提供一个视频供大家参考。  
 
    为了区别于我们常用的Class,因此给了个中文名类类型。目的就是为了知道此Class非彼Class。前面已经介绍了Class Loader,它的作用是根据提供的信息来加载类到内存中。我之前有提过这个类信息的提供者就是本篇博客要介绍的Class。提醒:一个类的Class对象和它的Instance是不一样的,切记,不然你会混乱的。开始了!

概念

    Class类是所有类(注意是对象)的共有信息的抽象,比如该类实现的接口、对应的加载器、类名等等。一句话,类类型保存了每个类所对应的类型信息。每一个类都有一个Class对象,这个对象在类被加载后由JVM自动构造。也是由JVM管理的,Class类是没有公共的构造方法的。
    Class对象对于类来说就像是,DNA对于每个人,里面有你的一切生物信息。java中可以通过Class来取得类的实例,也许将来的将来通过你的DNA也能得到你的另一个实例。科幻电影里是已经实现了。ok,概念应该有个初步的认识了。

常用方法

    方法的介绍本来不应该这么简单,但是发现一句两句的说不清楚,并且对于Java的理解有很好的帮助。所以临时决定这部分单独的写一篇博客。这里就简单的列几个,之前用过的方法。

  • forName:返回与带有给定字符串名的类或接口相关联的 Class 对象。
  • getName():一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。
  • newInstance():创建Class对象描述的类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。
  • getClassLoader():返回该类的类加载器。
  • getInterfaces():确定此对象所表示的类或接口实现的接口。
  • getComponentType():返回表示数组组件类型的 Class。
  • getSuperclass():返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class对象
  • isArray():判定此 Class 对象是否表示一个数组类。

 怎么得到

    获得Class对象的方法有三种        
    (1)利用Object.getClass()方法获取该对象的Class实例;
    (2)使用Class.forName()静态方法,用类的名字获取一个Class实例
    (3)运用类的.class的方式来获取Class实例,对于基本数据类型的封装类,还可以采用.TYPE来获取相对应的基本数据类型的Class实例
    这里需要注意的是虚拟机只会产生一份字节码, 用这份字节码可以产生多个实例对象。也就是说Class对象只会有一个。看如下代码:
    测试类
  1. public class Test {  
  2.    static {  
  3.        System.out.println("静态初始化");  
  4.    }  
  5.    {  
  6.        System.out.println("非静态初始化");  
  7.    }  
  8. }  
public class Test {
   static {
	   System.out.println("静态初始化");
   }
   {
	   System.out.println("非静态初始化");
   }
}
    客户端
  1. public class client {  
  2.     public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  3.                //方法1   
  4.        Class c=Class.forName("com.zjj.ClassTest.Test");  
  5.                //方法2   
  6.         c=Test.class;  
  7.                //方法3   
  8.         Test t=new Test();  
  9.         c=t.getClass();  
  10.                Test t2=new Test();  
  11.     }  
  12. }  
public class client {
    public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
               //方法1
       Class c=Class.forName("com.zjj.ClassTest.Test");
               //方法2
    	c=Test.class;
               //方法3
    	Test t=new Test();
    	c=t.getClass();
               Test t2=new Test();
    }
}
    输出结果为:
             静态初始化
      非静态初始化
      非静态初始化
    大家知道静态初始化方法是在类加载的时候执行的,非静态初始化方法是在类被实例化的时候执行的。而输出结果只打印了一次“静态初始化”,这就说明三次得到的Class对象都是同一个。

    也就是说,在运行期间,如果我们要产生某个类的对象或者的得到某个类的Class对象,Java虚拟机(JVM)会检查该类型的Class对象是否已被加载。如果没有被加载,JVM会根据类的名称找到.class文件并加载它。一旦某个类型的Class对象已被加载到内存,就可以用它来产生该类型的所有对象

结语

    本篇总结:至此,应该可以理解了Class也是一个类,只不过它是所有类的一个抽象,名字又和我们所知道的Class一样容易造成混淆。总的来说,每一个类都有对应的一个Class对象来保存这个类的信息,这个Class对象由JVM构造和管理。Class对象的存在是Java反射的基础。

    反射机制总结:反射机制是Java的一个重要的内容,为Java提供了运行时加载类的能力,也就是动态性。Class是信息提供者,Class Loader是加载工具,二者都是反射机制最基础的部分。那么所谓的反射就是解除耦合,方式就是通过Class取得未知类的信息,而后实例化。当然Class Loader的所做的工作是隐藏的,是Class对象去调用的。所以无需显示的自己调用。

 

 

    反射机制这几篇博客写下来发现涉及到Java类的加载机制,这部分的内容也比较独立的一部分,因此单另一篇来写。在JAVA中任何的类都是需要加载到JVM中才能运行的。之前Class Loader介绍了类的加载机制,那么这里要说的是不同加载方式之间的对比,好能对JAVA类的实例化过程有更深刻的体会。  

new和Class.newInstance

    我们说代码里出现new关键字意味着对于可能变动的代码,耦合过高了。遇到这种情况我们会用反射机制来去除new关键字,这在代理模式里我们见过了。实际上也就是用了Class.newInstance来代替。这说明这两种方式都可以得到相同的对象实例,但是它们之间存在区别,耦合度不同。
    实际上在理解上我们可以认为,Class.newInstanc方式来实例化对象是对new关键字的拆分成两步了。因为,Class.newInstance的使用是有前提的,要保证类已经加载到JVM中,并且已经链接。看如下代码:

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.         //从当前线程取得正在运行的加载器   
  3.         ClassLoader cl=Thread.currentThread().getContextClassLoader();  
  4.         cl.loadClass("com.zjj.ClassTest.Test");    //加载测试类到JVM   
  5.         Class c2=cl.getClass();         //得到类的Class对象   
  6.         c2.newInstance();               //实例化对象        
  7.     }  
  8. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    	//从当前线程取得正在运行的加载器
    	ClassLoader cl=Thread.currentThread().getContextClassLoader();
    	cl.loadClass("com.zjj.ClassTest.Test");    //加载测试类到JVM
    	Class c2=cl.getClass();         //得到类的Class对象
    	c2.newInstance();               //实例化对象    	
    }
}</span></span>

    这里不用Class.forName来得到Class对象是为了保证类被加载了但是没有被链接。 这段代码看着貌似没什么错,编译也没有问题,但是运行的时候就出错了。也就是说通过如上方法加载的类是没有被链接的,因此newInstance方法无法执行。
    前面说理解上可以简单的认为是通过Class.Instance方式是new拆分的两步,但是事实上new要比Class.Instance做的多。Class.Instance方法只能访问无参数的构造函数,new则都可以访问。建立一个有两个构造函数的测试类,看客户端调用代码:

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.         Class c=Class.forName("com.zjj.ClassTest.Test");  
  3.        c.newInstance();           
  4.         new Test("ni");       
  5.     }  
  6. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    	Class c=Class.forName("com.zjj.ClassTest.Test");
       c.newInstance();         
    	new Test("ni");    	
    }
}</span></span>

    输出结果为:
        无参数的构造函数
        带参数的构造函数
    如果在newInstance中传入参数去调用带参数的构造函数的话是会报错的,无法通过编译。相对来说newInstance是弱类型,new是强类型。

Class.forName和classLoad.loadClass

    讲这两个的区别之前我们先要了解,JVM会执行静态代码段,要记住一个概念,静态代码是和class绑定的,class装载成功就表示执行了静态代码了,以后也就不会再走这段静态代码了。 也就是说静态代码段是只会执行一次的,在类被加载的时候。另外我们还需要知道,类的加载过程分为装载、连接、初始化。还有就是,JVM遇到类请求时它会先检查内存中是否存在,如果不存在则去加载,存在则返回已存在的Class对象。
    那么这两个方法的区别就在于执行的这三个过程不一样。forName有两个函数(多态),三个参数时forName(String className, boolean initialize, ClassLoader loader)第二个参数为True时则类会链接,会初始化。为False时,如果原来不存在则一定不会连接和初始化,如果原来存在被连接的Class对象,则返回该对象但是依然不会初始化。单参数时,默认initialize是为True的。
    loadClass也是多态loadClass(String name)单参数时, resolve=false。如果该类已经被该类装载器所装载,那么,返回这个已经被装载的类型的Class的实例,否则,就用这个自定义的类装载器来装载这个class,这时不知道是否被连接。绝对不会被初始化!这时唯一可以保证的是,这个类被装载了。但是不知道这个类是不是被连接和初始化了。
    loadClass(String name, boolean resolve)resolve=true时,则保证已经装载,而且已经连接了。 resolve=falses时,则仅仅是去装载这个类,不关心是否连接了,所以此时可能被连接了,也可能没有被连接。下面通过测试来验证以上说的内容,代码如下:
    Test类:

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public class Test {  
  2.    static {  
  3.        System.out.println("静态初始化");  
  4.    }     
  5.    public Test(){  
  6.        System.out.println("无参数的构造函数");  
  7.    }  
  8.    public Test(String str){  
  9.        System.out.println("带参数的构造函数");  
  10.    }  
  11.    {  
  12.        System.out.println("非静态初始化");  
  13.    }  
  14. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public class Test {
   static {
	   System.out.println("静态初始化");
   }   
   public Test(){
	   System.out.println("无参数的构造函数");
   }
   public Test(String str){
	   System.out.println("带参数的构造函数");
   }
   {
	   System.out.println("非静态初始化");
   }
}</span></span>

    测试一:客户端调用代码

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.         Class c=Class.forName("com.zjj.ClassTest.Test");  
  3.      }  
  4. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    	Class c=Class.forName("com.zjj.ClassTest.Test");
     }
}</span></span>

    输出结果为:静态初始化
    说明:Class.forName时类执行了装载、连接、初始化三个步骤。
    测试二:客户端代码改为

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.         ClassLoader cl=Thread.currentThread().getContextClassLoader();  
  3.         Class c=Class.forName("com.zjj.ClassTest.Test"false, cl);  
  4.      }  
  5. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    	ClassLoader cl=Thread.currentThread().getContextClassLoader();
    	Class c=Class.forName("com.zjj.ClassTest.Test", false, cl);
     }
}</span></span>

    输出结果为:initialize=true时输出,静态初始化。为false时没有输出
    说明:为true时类执行了装载、连接、初始化三个步骤。为false时没有初始化,为知是不是连接。
    测试三:客户端代码改为

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.         ClassLoader cl=Thread.currentThread().getContextClassLoader();  
  3.         Class c=Class.forName("com.zjj.ClassTest.Test"false, cl);  
  4.                c.newInstance();  
  5.      }  
  6. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    	ClassLoader cl=Thread.currentThread().getContextClassLoader();
    	Class c=Class.forName("com.zjj.ClassTest.Test", false, cl);
               c.newInstance();
     }
}</span></span>

    输出结果为:
        静态初始化
        非静态初始化
        无参数的构造函数
    说明:为了保证JVM中不存在之前加载过的类,特地清理了JVM内存。但是输出结果不变,说明为false时执行了装载和链接,否则newInstance是无法执行的(前面说过了newInstance的执行条件)。但是资料说可能还存在不连接的情况!!有待考证。
    测试四:客户端代码改为

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{  
  2.                Class c=Class.forName("com.zjj.ClassTest.Test");  
  3.         ClassLoader cl=Thread.currentThread().getContextClassLoader();  
  4.         Class c=Class.forName("com.zjj.ClassTest.Test"true, cl);  
  5.              }  
  6. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
               Class c=Class.forName("com.zjj.ClassTest.Test");
    	ClassLoader cl=Thread.currentThread().getContextClassLoader();
    	Class c=Class.forName("com.zjj.ClassTest.Test", true, cl);
             }
}</span></span>

    输出结果为:静态初始化
    说明:如果原来存在加载过的类,那么第二次执行加载请求时返回存在的。因为,静态初始化只执行了一次。
    测试五:客户端代码改为

  1. <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{                
  2.                //从当前线程取得正在运行的加载器   
  3.         ClassLoader cl=Thread.currentThread().getContextClassLoader();  
  4.         cl.loadClass("com.zjj.ClassTest.Test");    //加载测试类到JVM   
  5.         Class c2=cl.loadClass("com.zjj.ClassTest.Test").getClass();         //得到类的Class对象   
  6.         c2.newInstance();               //实例化对象   
  7.              }  
  8. }</span></span>  
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{              
               //从当前线程取得正在运行的加载器
    	ClassLoader cl=Thread.currentThread().getContextClassLoader();
    	cl.loadClass("com.zjj.ClassTest.Test");    //加载测试类到JVM
    	Class c2=cl.loadClass("com.zjj.ClassTest.Test").getClass();         //得到类的Class对象
    	c2.newInstance();               //实例化对象
             }
}</span></span>

    输出结果:报错
    说明:此时loadClass方法加载到内存中的类是未连接的,当然不会初始化。因此也就没有“静态初始化”的输出。
    测试六:不知道为什么没有发现代码中的ClassLoader存在两个参数的loadClass方法。
    总结:至此方法对比结束,这篇博客主要是更细致的了解了JVM加载类的过程和不同方式之间的区别。其实际上只是封装的程度不一样,也就是方法的粒度的差别。当然,有一点内容还没有通过自己的测试得到验证,可能是我的方法不对或者是资料有问题。权且记下这个问题!



         

上一篇:【Kotlin】扩展属性 ( 扩展变量属性 | 扩展常量属性 | 注意事项 | 本质分析 )


下一篇:iOS-《编写高质量代码》笔记-第八章 定制init...和dealloc