本文面向
另外,本文暂未对GC机制深入介绍,只有概览。GC党请先绕道 JDK,JRE和 JVM 的区别
上图表示了Java代码是怎么编译和加载的 整个流程从 Java 源码开始,经过 javac 程序处理后得到类文件,这个文件中保存的是编译源码后得到的 Java 字节码。类文件是 Java 平台能处理的最小功能单位,也是把新代码传给运行中程序的唯一方式。 新的类文件通过类加载机制载入虚拟机,从而把新类型提供给解释器执行。 Object的方法所有类都直接或间接扩展 java.lang.Object 类。这个类定义了很多有用的方法,而且你编写的类可以覆盖这些方法。 toString方法toString 方法的作用是返回对象的文本表示形式。连接字符串或使用 System.out.println 等方法时,会自动在对象上调用这个方法。给对象提供文本表示形式,十分利于调试或记录日志,而且精心编写的 toString 方法还能给报告生成等任务提供帮助。 Object 类中的 toString 方法返回的字符串由对象所属的类名和对象的十六进制形式哈希码(由 hashCode 方法计算得到,本章稍后会介绍)组成。这个默认的实现方式提供了对象的类型和标识两个基本信息,但一般并没什么用。 equals方法== 运算符测试两个引用是否指向同一个对象。如果要测试两个不同的对象是否相等,必须使用 equals 方法。任何类都能覆盖 equals 方法,定义专用的相等比较方式。Object.equals 方法直接使用 == 运算符,只有两个对象是同一个对象时,才判定二者相等。 hashCode方法Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。 如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了。hashcode方法的作用只要覆盖了 equals 方法,就必须覆盖 hashCode 方法。hashCode 方法返回一个整数,用于哈希表数据结构。如果两个对象经 equals 方法测试是相等的,它们就要具有相同的哈希码。不相等的对象要具有不相等的哈希码(为了哈希表的操作效率),这一点很重要,但不是强制要求,最低要求是不相等的对象不能共用一个哈希码。为了满足最低要求,hashCode 方法要使用稍微复杂的算法或位操作。 Object.hashCode 方法和 Object.equals 方法协同工作,返回对象的哈希码。这个哈希码基于对象的身份生成,而不是对象的相等性。(如果需要使用基于身份的哈希码,可以通过静态方法 System.identityHashCode 获取 Object.hashCode 方法的返回值。) HashCode和equal方法
Comparable::compareTo方法如果一个类实现了 Comparable 接口,就可以比较一个实例是小于、大于还是等于另一个实例。这也表明,实现 Comparable 接口的类可以排序。 因为 compareTo 方法不在 Object 类中声明,所以由每个类自行决定实例能否排序。如果能排序就定义 compareTo 方法,实现实例排序的方式。 compareTo 方法返回一个 int 类型的值,这个值需要进一步说明。如果当前对象(this)小于传入的对象,compareTo 方法应该返回一个负数;如果两个对象相等,应该返回 0;如果当前对象大于传入的对象,应该返回一个正数。 clone方法Object 类定义了一个名为 clone 的方法,这个方法的作用是返回一个对象,并把这个对象的字段设为和当前对象一样。clone 方法不常用,原因有两个。其一,只有类实现了 java.lang.Cloneable 接口,这个方法才有用。Cloneable 接口没有定义任何方法(是个标记接口),因此若想实现这个接口,只需在类签名的 implements 子句中列出这个接口即可。其二,clone 方法声明为 protected,因此,如果想让其他类复制你的对象,你的类必须实现 Cloneable 接口,并覆盖 clone 方法,而且要把 clone 方法声明为 public。 clone 方法很难正确实现,而副本构造方法实现起来更容易也更安全。 finalize方法一种古老的资源管理技术叫终结(finalization),开发者应该知道有这么一种技术。然而,这种技术几乎完全废弃了,任何情况下,大多数 Java 开发者都不应该直接使用。 只有少数应用场景适合使用终结,而且只有少数 Java 开发者会遇到这种场景。如果有任何疑问,就不要使用终结,处理资源的 try 语句往往是正确的替代品。 终结机制的作用是自动释放不再使用的资源。垃圾回收自动释放的是对象使用的内存资源,不过对象可能会保存其他类型的资源,例如打开的文件和网络连接。垃圾回收程序不会为你释放这些额外的资源,因此,终结机制的作用是让开发者执行清理任务,例如关闭文件、中断网络连接、删除临时文件,等等。 终结机制的工作方式是这样的:如果对象有 finalize 方法(一般叫作终结方法),那么不再使用这个对象(或对象不可达)后的某个时间会调用这个方法,但要在垃圾回收程序回收分配给这个对象的空间之前调用。终结方法用于清理对象使用的资源。 另外注意,这是一个实例方法。而在类上,没有等效的机制。 引用类型与基本类型比较引用类型和对象与基本类型和基本值有本质的区别。
把对象赋值给变量或传入方法时,不会复制表示这个对象的内存,而是把这个内存的引用存储在变量中或传入方法。 在 Java 中,引用完全不透明,引用的表示方式由 Java 运行时的实现细节决定。如果你是 C 程序员的话,完全可以把引用看作指针或内存地址。不过要记住,Java 程序无法使用任何方式处理引用。 似乎看的有点晕?来点儿代码吧! 下述代码处理 int 类型基本值: 执行这两行代码后,变量 y 中保存了变量 x 中所存值的一个副本。在 Java 虚拟机内部,这个 32 位整数 42 有两个独立的副本。 现在,想象一下把这段代码中的基本类型换成引用类型后再运行会发生什么: Point p =newPoint(1.02.0); Point q = p; 运行这段代码后,变量 q 中保存了一份变量 p 中所存引用的一个副本。在虚拟机中,仍然只有一个 Point 对象的副本,但是这个对象的引用有两个副本——这一点有重要的含义。假设上面两行代码的后面是下述代码: System.out.println(p.x); // 打印p的x坐标:1.0q.x =13.0; // 现在,修改q的x坐标// 再次打印p.x,这次得到的值是13.0 因为变量 p 和 q 保存的引用指向同一个对象,所以两个变量都可以用来修改这个对象,而且一个变量中的改动在另一个变量中可见。数组也是一种对象,所以对数组来说也会发生同样的事,如下面的代码所示: // greet保存一个数组的引用char greet = {'h''e''l''l''o'};char cuss = greet; // cuss保存的是同一个数组的引用cuss[4] ='!'; // 使用引用修改一个元素System.out.println(greet); // 打印“hell!” 把基本类型和引用类型的参数传入方法时也有类似的区别。假如有下面的方法: voidchangePrimitive(intx){ while(x >0) { System.out.println(x--); } } 调用这个方法时,会把实参的副本传给形参 x。在这个方法的代码中,x 是循环计数器,向零递减。因为 x 是基本类型,所以这个方法有这个值的私有副本——这是完全合理的做法。 可是,如果把这个方法的参数改为引用类型,会发生什么呢? voidchangeReference(Point p){ while(p.x >0) { System.out.println(p.x--); } } 调用这个方法时,传入的是一个 Point 对象引用的私有副本,然后使用这个引用修改对应的 Point 对象。例如,有下述代码: Point q =newPoint(3.04.5);// 一个x坐标为3的点changeReference(q); // 打印3,2,1,而且修改了这个Point对象System.out.println(q.x); // 现在,q的x坐标是0! 调用 changeReference 方法时,传入的是变量 q 中所存引用的副本。现在,变量 q 和方法的形参 p 保存的引用指向同一个对象。这个方法可以使用它的引用修改对象的内容。但是要注意,这个方法不能修改变量 q 的内容。也就是说,这个方法可以随意修改引用的 Point 对象,但不能改变变量 q 引用这个对象这一事实。 那么在用运算符:==时,也会有差别。 相等运算符(==)比较基本值时,只测试两个值是否一样(即每一位的值都完全相同)。而 == 比较引用类型时,比较的是引用而不是真正的对象。也就是说,== 测试两个引用是否指向同一个对象,而不测试两个对象的内容是否相同。 神奇的数组数组类型不是类,但数组实例是对象。这意味着,数组从 java.lang.Object 类继承了方法。数组实现了 Cloneable 接口,而且覆盖了 clone 方法,确保数组始终能被复制,而且 clone 方法从不抛出 CloneNotSupportedException 异常。数组还实现了 Serializable 接口,所以只要数组中元素的类型能被序列化,数组就能被序列化。而且,所有数组都有一个名为 length 的字段,这个字段的修饰符是 public final int,表示数组中元素的数量。 因为数组扩展自 Object 类,而且实现了 Cloneable 和 Serializable 接口,所以任何数组类型都能放大转换成这三种类型中的任何一种。而且,特定的数组类型还能放大转换成其他数组类型。如果数组中的元素类型是引用类型 T,而且 T 能指定给类型 S,那么数组类型 T 就能指定给数组类型 S。注意,基本类型的数组不能放大转换。例如,下述代码展示了合法的数组放大转换: String arrayOfStrings; // 创建字符串数组int arrayOfArraysOfInt; // 创建int二维数组Object oa = arrayOfStrings;// String可以指定给Object,因此String可以指定给ObjectComparable ca = arrayOfStrings;// String实现了Comparable接口,因此String可以视作ComparableObject oa2 = arrayOfArraysOfInt;// int是Object类的对象,因此int可以指定给Object// 所有数组都是可以复制和序列化的对象Object o = arrayOfStrings; Cloneable c = arrayOfArraysOfInt; Serializable s = arrayOfArraysOfInt[0]; 因为数组类型可以放大转换成另一种数组类型,所以编译时和运行时数组的类型并不总是一样。这种放大转换叫作“数组协变”(array covariance)。 自动装箱(autoboxing)与拆箱(unboxing)自动装箱是 Java 编译器在基本数据类型和对应的对象包装类型之间做的一个转化。 基本类型和引用类型的表现完全不同。有时需要把基本值当成对象,为此,Java 平台为每一种基本类型都提供了包装类。Boolean、Byte、Short、Character、Integer、Long、Float 和 Double 是不可变的最终类,每个实例只保存一个基本值。包装类一般在把基本值存储在集合中时使用。 例如
把 int 转化成 Integer,double 转化成 Double等,反之就是自动拆箱。 Integer a=1;//这就是一个自动装箱,如果没有自动装箱的话,需要这样Integer a=new Integer(1) intb=a;//这就是一个自动拆箱,如果没有自动拆箱的话,需要这样:int b=a.intValue 这样就能看出自动装箱和自动拆箱是简化了基本数据类型和相对应对象的转化步骤。 Java中的自动装箱与拆箱 Java中为什么要为基本类型提供封装类呢?是为了在各种类型间转化,通过各种方法的调用。否则你无法直接通过变量转化。 比如,现在int要转为String inta=0; String result=Integer.toString(a); 比如现在要用泛型 List<Integer> nums; 这里<>就需要一个类。如果用int,则会报错的。 关于异常
异常抛出
捕捉的异常时,不要仅仅调用printStackTreace去打印输出,应该添加事务回滚等操作。catch(Exception)可以捕捉遗漏的异常。最后在finally语句里记得释放资源。 关于Java反射——ClassTypejava中,静态成员和普通数据类型不是对象,其他皆是。 那么问题来了,类是谁的对象?java.lang.Class的实例对象。
序列化(Serializable )与反序列化(Deserialize)对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程。 简单的来说就是从object变成了byte。方便传输。 序列化一般用于以下场景:
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。 小Tips:对子类对象进行反序列化操作时,如果其父类没有实现序列化接口,那么其父类的构造函数会被显式的调用。 java.io.ObjectOutputStream代表对象输出流,它的writeObject(Objectobj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 java.io.ObjectInputStream代表对象输入流,它的readObject方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。 覆盖 (Override) 和重载 (Overload)Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况; 方法覆盖是说子类重新定义了父类的方法,方法覆盖必须有相同的方法名,参数列表和返回类型。 内存中的栈(stack)、堆(heap)和静态存储区的用法通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过new关键字和构造器创建的对象放在堆空间;程序中的字面量(literal)如直接书写的100、“hello”和常量都是放在静态存储区中。栈空间操作最快但是也很小,通常大量的对象都是放在堆空间,整个内存包括硬盘上的虚拟内存都可以被当成堆空间来使用。 String str = new String(“hello”); 上面的语句中 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而“hello”这个字面量放在静态存储区。 强引用、弱引用、软引用、虚引用
Java垃圾回收机制与引用类型 Java垃圾回收机制在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾收集意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾收集也可以清除内存记录碎片。由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。 垃圾收集能自动释放内存空间,减轻编程的负担。这使Java虚拟机具有一些优点。首先,它能使编程效率提高。在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。其次是它保护程序的完整性, 垃圾收集是Java语言安全性策略的一个重要部份。垃圾收集的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾收集算法的不完备性,早先采用的某些垃圾收集算法就不能保证100%收集到所有的废弃内存。当然随着垃圾收集算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。一般来说,Java开发人员可以不重视JVM中堆内存的分配和垃圾处理收集,但是,充分理解Java的这一特性可以让我们更有效地利用资源。同时要注意finalize方法是Java的缺省机制,有时为确保对象资源的明确释放,可以编写自己的finalize方法。(引用自百度)Java 垃圾收集机制集合框架对象存入集合时会变成Object类型,取出时需要类型转换。所以会有泛型(这样也不用考虑取出时的类型转换了)。另外集合里存储的是引用,所以泛型不能使用基本类型。 上图为常见集合 上图为集合概览 上图为集合家族的关系 Collection、Set、List、Map、SortedSet 和 SortedMap 都是接口,不过 java.util 包定义了多个具体实现,例如基于数组和链表的列表,基于哈希表或二叉树的映射和集。除此之外,还有两个重要的接口,Iterator 和 Iterable,用于遍历集合中的对象。 Collection接口Collection 是参数化接口,表示由泛型 E 对象组成的集合。这个接口定义了很多方法,用来把对象添加到集合中,把对象从集合中移除,测试对象是否在集合中,以及遍历集合中的所有元素。还有一些方法可以把集合中的元素转换成数组,以及返回集合的大小。 Set接口集(set)是无重复对象组成的集合:不能有两个引用指向同一个对象,或两个指向 null 的引用,如果对象 a 和 b 的引用满足条件 a.equals(b),那么这两个对象也不能同时出现在集中。多数通用的 Set 实现都不会对元素排序,但并不禁止使用有序集(SortedSet 和 LinkedHashSet 就有顺序)。而且集与列表等有序集合不同,一般认为,集的 contains 方法,不论以常数时间还是以对数时间评判 1,运行效率都高。 List接口List 是一组有序的对象集合。列表中的每个元素都有特定的位置,而且 List 接口定义了一些方法,用于查询或设定特定位置(或叫索引)的元素。从这个角度来看,List 对象和数组类似,不过列表的大小能按需变化,以适应其中元素的数量。和集不同,列表允许出现重复的元素。 除了基于索引的 get 和 set 方法之外,List 接口还定义了一些方法,用于把元素添加到特定的索引,把元素从特定的索引移除,或者返回指定值在列表中首次出现或最后出现的索引。从 Collection 接口继承的 add 和 remove 方法,前者把元素添加到列表末尾,后者把指定值从列表中首次出现的位置移除。继承的 addAll 方法把指定集合中的所有元素添加到列表的末尾,或者插入指定的索引。retainAll 和 removeAll 方法的表现与其他 Collection 对象一样,如果需要,会保留或删除多个相同的值。 List 接口没有定义操作索引范围的方法,但是定义了一个 subList 方法。这个方法返回一个 List 对象,表示原列表指定范围内的元素。子列表会回馈父列表,只要修改了子列表,父列表立即就能察觉到变化。 Map接口映射(map)是一系列键值对,一个键对应一个值。Map 接口定义了用于定义和查询映射的 API。Map 接口属于 Java 集合框架,但没有扩展 Collection 接口,因此 Map 只是一种集合,而不是 Collection 类型。Map 是参数化类型,有两个类型变量。类型变量 K 表示映射中键的类型,类型变量 V 表示键对应的值的类型。例如,如果有个映射,其键是 String 类型,对应的值是 Integer 类型,那么这个映射可以表示为 Map<String,Integer>。 Map 接口定义了几个最有用的方法:put 方法定义映射中的一个键值对,get 方法查询指定键对应的值,remove 方法把指定的键及对应的值从映射中删除。一般来说,实现 Map 接口的类都要能高效执行这三个基本方法:一般应该运行在常数时间中,而且绝不能比在对数时间中运行的性能差。 Map 的重要特性之一是,可以视作集合。虽然 Map 对象不是 Collection 类型,但映射的键可以看成 Set 对象,映射的值可以看成 Collection 对象,而映射的键值对可以看成由 Map.Entry 对象组成的 Set 对象。(Map.Entry 是 Map 接口中定义的嵌套接口,表示一个键值对。) Queue接口和BlockingQueue接口队列(queue)是一组有序的元素,提取元素时按顺序从队头读取。队列一般按照插入元素的顺序实现,因此分成两类:先进先出(first-in, first-out,FIFO)队列和后进先出(last-in, first-out,LIFO)队列。 LIFO 队列也叫栈(stack),Java 提供了 Stack 类,但强烈不建议使用——应该使用实现 Deque 接口的类。 队列也可以使用其他顺序:优先队列(priority queue)根据外部 Comparator 对象或 Comparable 类型元素的自然顺序排序元素。与 Set 不同的是,Queue 的实现往往允许出现重复的元素。而与 List 不同的是,Queue 接口没有定义处理任意索引位元素的方法,只有队列的头一个元素能访问。Queue 的所有实现都要具有一个固定的容量:队列已满时,不能再添加元素。类似地,队列为空时,不能再删除元素。很多基于队列的算法都会用到满和空这两个状态,所以 Queue 接口定义的方法通过返回值表明这两个状态,而不会抛出异常。具体而言,peek 和 poll 方法返回 null 表示队列为空。因此,多数 Queue 接口的实现不允许用 null 作元素。 阻塞式队列(blocking queue)是一种定义了阻塞式 put 和 take 方法的队列。put 方法的作用是把元素添加到队列中,如果需要,这个方法会一直等待,直到队列中有存储元素的空间为止。而 take 方法的作用是从队头移除元素,如果需要,这个方法会一直等待,直到队列中有元素可供移除为止。阻塞式队列是很多多线程算法的重要组成部分,因此 BlockingQueue 接口(扩展 Queue 接口)在 java.util.concurrent 包中定义。 用户线程(User Thread)与守护线程(Daemon Thread)守护线程,是指用户程序在运行的时候后台提供的一种通用服务的线程。只要当前JVM实例中尚存在任何一个用户线程没有结束,守护线程就全部工作;只有当最后一个用户线程结束时,守护线程随着 JVM 一同结束工作。 守护线程最典型的应用就是 GC (垃圾回收器)。 点击文章右上方的图标,分享到“朋友圈”你的分享会让精彩传播给更多的人。 点击左下角阅读原文查看更多长知识文章! |
|