Android面试汇总

APP –> Framework –> 底软
学习 –> 使用 –> 实现原理 –> 形成自己的理解 –> 给自己留下烙印

A:基础
通过关键字来梳理这些相关的知识点

  1. 开关机流程
  1. ActivityManagerService
    Activity启动流程, Instrumentation类提供的各种流程控制方法
    ActivityStack
    ActivityThread

  2. PackageManagerService

  3. WindowManagerService

  4. 进程通讯IPC的方式:
    Linux中有pipe管道,命名管道FIFO,内存映射(mapped memeory),消息队列(message queue),内存共享(shared memeory),信号量(semaphore),信号(signal),套接字(socket) 等八种方式.
    Binder
    Serializable 和 Parcelable 进行序列化
    AIDL
    Messager
    Android中IPC的方式:
    1.Bundle
    2.文件共享
    3.Messenger
    4.AIDL
    5.ContentProvider
    6.Socket

  1. JNI
  1. View
    View的工作原理,Event事件
    measure 测绘 测量View的宽和高
    layout 布局 确定View最终宽和高,以及四个角的顶点
    draw 绘制

  2. SeLinux

  1. ANR
  2. Crash
  3. Tombstone
  4. Freeze
  5. OOM
  6. LMK
  7. 卡顿
  8. 省电
  9. 堆和栈
    堆内存和栈内存的概念
    栈内存:系统自动分配和释放,
    保存全局、静态、局部变量,
    在站上分配内存叫静态分配,
    大小一般是固定的
    堆内存:程序员手动分配(malloc/new)和释放(free/java不用手动释放,由GC回收),
    在堆上分配内存叫动态分配,
    一般硬件内存有多大堆内存就有多大

一些底层基本知识(Android篇二)

  1. Android的线程和线程池
    AsyncTask:封装了线程池和Handler,主要是为了方便开发者在子线程中更新UI
    IntentService
    HandlerThread
    线程池

底软模块:

B:做过哪些工作

功耗、性能、稳定性三大块

性能优化:

Android 性能优化之内存检测、卡顿优化、耗电优化、APK瘦身
Android性能优化系列

一些有效的性能优化方法:布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bitmap优化、线程优化。
布局优化:尽量减少布局的层级
首先删除布局中无用的控件和层级,其次有选择地使用性能较低的ViewGroup,比如RelativeLayout。尽量使用性能较高的ViewGroup如LinearLayout。
另一种手段就是采用标签、标签和ViewStub.标签主要用于布局重用,标签和标签配合使用,降低减少布局的层级。而ViewStub则提供了按需加载功能,提高了程序的初始化效率。
绘制优化:
绘制优化是指View的onDraw方法要避免执行大量的操作。主要体现在两个方面:

  1. onDraw中不要创建新的局部对象。
  2. onDraw中不要做耗时的任务。
    内存泄漏优化:
    Android应用内存泄露分析、改善经验总结

响应速度优化:
ListView优化:
Bitmap优化:
线程优化:

性能优化用到的工具
Android Studio的 Analyze-Inspect Code 对代码做静态分析,查找常见的内存泄漏问题
Android Sutdio自带的代码检查工具analyze的使用

代码检查的工具:

性能测试工具:
1.APP启动时间
Android性能测试 | 启动时间篇
Android性能优化——优化应用启动时间 Google官网的翻译
Android 性能优化—(8)APP启动时间优化指南

思路: 启动方式/原理 –> 查看启动时间的方式 –> 查看启动时间的工具 –> 启动时间的解决方案

APP启动方式:
a).冷启动:需要新建用户进程
冷启动时,系统需要执行三个任务
1.加载和启动应用程序,
2.在app启动后立即显示一个空白启动窗体
3.创建App进程。
一旦系统创建了应用进程,应用进程就会执行下面步骤:
1.创建应用程序对象
2.启动主线程
3.创建Main Activity
4.初始化构造view
5.在屏幕上布局。
6.执行初始化绘制操作
b).热启动:
c).暖启动:

查看启动时间的方式:
a) 通过log中的Displayed字段查找
b) adb shell am start -S -W com.example.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN
比如:adb shell am start -S -W com.android.systemui/.recents.RecentsActivity
c) 通过reportFullyDrawn函数确定完全显示耗时—针对应用有延长的情况比较有用
d)

启动时间查看工具:
a) Trace函数以及Systrace工具
b) AndroidStudio 中的 Method Tracer作用就是监听一段时间内,某个进程的某个线程,执行的所有方法,以及各个方法所消耗的时间 在Android Studio中使用Method trace,查看某进程的所有线程trace的方法

APP启动时间实例:
a)初始化开销大
Application 的创建过程中,如果执行复杂的逻辑或者初始化大量的对象,将会影响应用的启动体验
解决方案:

2.开机启动时间

Android 系统性能优化(58)—开机时间优化
Android O 启动优化
Android 开机速度优化—–ART 预先优化

Android性能优化之路(一)
检测卡顿工具:
a)TraceView
b)MethodProfiling
c)Profile GPURendering(GPU显示配置文件) 在开发者工具里面有开关可以打开,

Android系统的启动时间

3.内存
思路: 内存查看 –> 内存泄漏与OOM的常见方式 –> 检测工具 –> 如何优化 –> 自己的实战情况

Android客户端性能测试—内存(一)
android程序内存被分为2部分:native和dalvik,dalvik就是我们平常说的java堆,我们创建的对象是在这里面分配的,而bitmap是直接在native上分配的,对于内存的限制是 native+dalvik 不能超过最大限制。android程序内存一般限制在16M,当然也有24M的

两条命令查看内存:
a) adb shell dumpsys meminfo PID/包名 -d
在进入一个界面之前查看一遍Activity和View的数量,在退出这个界面之后再查看一遍Activity和View的数量,对比进入前和进入后Activity和View数量的变化情况,如果有差异,则说明存在内存泄露(在使用命令查看Activity和View的数量之前,记得手动触发GC)。
手动触发GC的方式:1.DDMS中有个按钮 2.代码中调用System.gc(); 3.当创建一个内存分析文件HPROF时触发,使用adb shell am dumpheap可以触发分析HPROF文件

b)adb shell procrank
Procrank可以同时获得以下几种内存的信息: VSS:Virtual Set Size虚拟耗用内存(包含共享库占用的内存)。This size also includes memory that may not be resident in RAM like mallocs that have been allocated but not written to,所以用VSS来衡量一个进程实际使用的内存意义不大。

RSS : Resident Set Size实际使用物理内存(包含共享库占用的内存)。 RSS can be misleading, because it reports the total all of the shared libraries that the process uses, even though a shared library is only loaded into memory once regardless of how many processes use,所以用RSS来衡量进程占用的内存信息不是特别准确。

PSS - Proportional Set Size实际使用的物理内存(比例分配共享库占用的内存)。 PSS is a very useful number because when the PSS for all processes in the system are summed together, that is a good representation for the total memory usage in the system,所以用pss衡量程序占用的内存误差比较小。

USS - Unique Set Size进程独自占用的物理内存(不包含共享库占用的内存). USS is an extremely useful number because it indicates the true incremental cost of running a particular process,所以用USS描述进程占用内存的波动和峰值比较有意义。

一般存在以下关系VSS>=RSS>=PSS>=USS。
c) adb shell getprop|grep heapgrowthlimit 查看最大内存
d) adb shell top 列出top*进程的cpu和内存占用情况,默认按照cpu占用降序排列。top可以获得进程的VSS和RSS信息,命令持续的监视

内存泄漏和OOM内存溢出
通过命令或者工具,如果发现内存一直在增长,说明发生了内存泄漏.
OOM内存溢出

常见的内存泄漏,可以用代码静态扫描找出来.
常见的代码静态扫描工具:
Android 静态代码扫描流程及工具说明
1.Android Lint
2.Godeyes
3.Infer

常见的代码扫描案例:
[空指针]空指针引用
[内存泄露]Stream资源关闭
[性能]使用indexOf(字符)
[兼容]系统API兼容性隐患
[越界]数组下标越界隐患
[异常] 使用除法或求余没有判断分母长度隐患
[SQL]注入风险
[应用安全] AndroidMannifest.xml文件中allowBackup设置为true时会导致数据泄露

常见的内存泄漏:
1.IO操作后,没有关闭文件导致的内存泄露,比如Cursor、FileInputStream、FileOutputStream使用完后没有关闭
2.自定义View中使用TypedArray后,没有recycle
3.某些地方使用了四大组件的context,在离开这些组件后仍然持有其context导致的内存泄露,这种问题属于共识,在编写代码的过程中就应该按照规则来,使用Application的Context就可以解决这类内存泄露的问题了

分析内存泄漏的工具
1.MAT Android 性能优化之使用MAT分析内存泄露问题
2.Android Studio的 Analyze-Inspect Code对代码做静态分析
3.Leakcanary

内存泄漏优化:
Android应用内存泄露分析、改善经验总结
Android 内存泄露优化处理

3.CPU
安卓性能指标cpu主要关注两点:
(1)cpu总体使用率(2)应用程序cpu占用率。

CPU利用率:CPU执行非系统空闲进程的时间 / CPU总的执行时间。
Android关于进程使用率的限制:前台进程不超过95%,后台进程5%, 但是在系统没有前台进程时,后台进程可以超过5%。
cpu使用过高,可能引发的问题
1)整体性能降低
2)界面卡顿
3)响应慢,容易引起ANR
4)手机发热
CPU测试项
1.空闲状态下的应用CPU消耗情况
2.中等规格状态下的应用CPU消耗情况
3.满规格状态下的应用CPU消耗情况
4.应用CPU峰值情况
查看方式

  1. adb shell dumpsys cpuinfo|grep ‘
    2.安卓sdk中tools下找到ddms,启动查看
    Android性能测试之cpu占用率
    Android CPU性能文件位置
    Android客户端性能测试—CPU、启动时间(二)
    上面的三篇文章都提到了使用adb相关的命令来查看CPU的情况
    adb shell echo 3>/proc/sys/vm/drop_caches 可以清除系统cache

安卓性能测试之cpu占用率统计方法总结

设置CPU核心数和频率
Android下设置CPU核心数和频率 通过变化online的核心数和调整CPU频率可以做到功耗和性能的特殊要求。
分享一个最全的Android的CPU各种模式设置及作用

设置I/O调度
Android手机I/O调度模式简介
分享一个最全的Android的I/O调度各种模式设置及作用

解决CPU使用率高的实例?
Android CPU使用过大的问题解决以及造成的原因

Android性能测试|流量、电量、弱网环境的测试方法

4.耗电量
APP耗电的原因:
1.http请求(GZIP压缩)
2.json数据解析(json解析效率主要是解析耗时),大量的数据解析
3.数据库读写操作
4.SD卡读写操作
5.程序的执行的timer定时器(例如IM中的心跳包,用系统的Alarm优化)
6.网络切换(wifi会比手机的数据移动网络更加省电)
针对以上几种原因的耗电,分别处理
Android App耗电分析

系统耗电的原因:
1.蓝牙耗电
2.wifi耗电
3.移动数据耗电
4.CPU耗电
5.摄像头耗电
6.GPS耗电
7.手电筒耗电
8.wakelock耗电
9.各种Sensor耗电
Android性能专项测试之耗电量统计API

耗电分析工具:
1.BatteryHistorian 由Google提供的Android系统电量分析工具,从手机中导出bugreport文件上传至页面,在网页中生成详细的图表数据来展示手机上各模块电量消耗过程,最后通过App数据的分析制定出相关的电量优化的方法。
Android应用耗电量分析与优化建议
电量优化的工具battery-historien
Android应用耗电问题排查

5.流量
获取流量信息的方法:
【Android】性能测试之获取Android流量数据
Android 性能测试实践 (四) 流量

网络的优化
Android App优化之网络优化
优化网络流量消耗 来自Android官网

弱网优化

网络优化工具:
1.tcpdump抓包
2.Wireshark抓包
Android的app性能测试–流量

6.流畅度
又叫FPS,或者刷新率VS帧率
刷新率:每秒屏幕刷新次数,手机屏幕的刷新率是60HZ
帧率:GPU在一秒内绘制的帧数
撕裂
因为屏幕的刷新过程是自上而下、自左向右的,如果帧率>刷新率,当屏幕还没有刷新n-1帧的数据时,就开始生成第n帧的数据了,从上到下,覆盖第n-1帧。如果此时刷新屏幕,就会出现图像的上半部分是第n帧的,下半部分是第n帧的现象。CPU/GPU一直都在渲染。
丢帧
Android系统每隔16ms发出VSYNC信号,触发GPU对UI进行渲染,如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候由于还没有准备好,就无法进行更新任何内容,那么用户在32ms内看到的会是同一帧画面(卡顿现象),即丢帧现象。
Android App性能评测分析-流畅度篇

Android流畅度总结

稳定性:
1.ANR
2.Crash
3.Tombstone
4.Freeze
5.黑屏
6.冻屏

功耗:

Java面试题

Java基础

  1. 集合类以及集合框架;HashMap与HashTable实现原理,线程安全性,hash冲突以及处理算法,ConcurrentHashMap
  1. 进程和线程的区别
  1. Java的并发、多线程、线程模型

  2. 什么是线程池,如何使用
    线程池就是事先将多个线程对象放到一个容器中,当使用额时候就不用new线程,而是直接去线程池中拿线程就可以了,节省了开辟子线程的时间,提高代码的执行效率。

  3. 数据一致性如何保证;Synchronized关键字,类锁,方法锁和重入锁

  4. Java中实现多态的机制是什么

  5. 如何将一个Java对象序列化到文件中

  6. 说说对Java反射的理解
    Java中的反射,首先是能够获取到Java中要反射类的字节码,获取字节码有三种方式
    a)Class.forName(classname)
    b)类名.class
    c) this.getClass()
    然后将字节码中的方法、变量、构造函数等映射成相应的Method、Filed、Constructor等
    d)同步的方法:多进程开发以及多进程应用场景

  7. 在Java中wait和sleep方法的不同
    最大的不同是在等待的时候wait会释放锁,而sleep一直持有锁。wait通常用于线程间的交互,sleep通常用于执行暂停

  8. synchronized和volatile关键字的作用
    1) 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的

  1. 服务器只提供数据接收接口,在多线程或多线程条件下,如何保证数据的有序到达

  2. TheadLocal原理,实现以及如何保证Local属性

  3. String、StringBuilder、StringBuffer的对比

  4. 你所知道的设计模式

  5. Java如何调用C、C++语言

  6. 接口和回调,回调的原理,写一个回调的demo

  7. 泛型的原理,举例说明;解析与分派

  8. 抽象类和接口的区别;应用场景,抽象类是否可以没有方法和属性

  9. 静态属性和静态方法是否可以被继承?是否可以被重写?什么原因

  10. 修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法

  11. 说说对泛型的理解

  12. Java的异常体系

  13. 如何控制某个方法运行并发访问线程的个数

  14. 动态代理的区别,分别在什么场景下使用

Android面试题

Android基础

  1. 什么是ANR?如何避免它?
    看《ANR机制以及问题分析》

  2. View的绘制流程?自定义View如何考虑机型适配? View的事件分发机制?

  3. ART和Dalvik的对比,JVM的内存 分配和垃圾回收GC机制,虚拟机原理,如何自己设计一个虚拟机(内存管理,类加载,双亲委派),JVM的内存模型以及类加载机制,内存对象的循环应用以及避免

  1. DDMS/traceView/systrace
  1. 内存回收机制与GC算法(各种算法的优缺点以及应用场景);GC的原理时机以及GC对象;内存泄漏的场景以及解决方法
  1. 四大组件以及生命周期;ContentProvider的权限管理(读写分离,权限控制-精确到表级,URL控制),Activity的四种启动模式的对比,Activity状态保存于恢复
  1. 什么是AIDL,以及如何使用.
  1. 请解释下在单线程模型中Message、Hanlder、MessageQueue、Looper之间的关系
  1. Fragment生命周期,Fragment状态保存,startActivityForResult是哪个类的方法,在什么情况下使用,如果在Adapter中使用,应该如何解耦
  1. AsyncTask 原理以及不足,IntentService的原理
  1. Activity怎么和Service绑定,怎么在Activity中启动对应的Service

  2. AsyncTask + HttpClient 与 AsyncHttpClient有什么区别

  3. 请描述一下Service的生命周期

  1. 如何保证一个后台服务不被杀死;比较省电的方式是什么?

  2. 如何通过广播拦截和abort一条短信;广播是否可以请求网络,广播引起的ANR的时间限制

  3. 进程间通信的方式有哪些? AIDL

  4. 事件分发中的onTouch和onTouchEvent有什么区别,又该如何使用

  1. 说说 ContentProvider/ContentResolver/ContentObservr之间的关系

  2. 请介绍一下ContentProvider是如何实现数据共享的

  3. Handler的机制以及底层的实现

  4. Binder机制以及底层的实现

  5. ListView中图片错位的问题是如何产生的

  6. 在manifest和代码中是如何注册和使用广播的

  7. 说说Activity/Intent/Service是什么关系

  8. ApplicationContext和ActivityContext的区别是什么

  1. 一张Bitmap所占内存以及内存占用的计算方法

  2. Serializable和Parcelable的区别

  3. 请描述一下BroadcastReceiver

  1. 请描述一下Android的事件分发机制
  1. 请介绍一下NDK

  2. 什么是NDK库,如何在JNI中注册native函数,有几种注册方式

  3. Async如何使用

  1. 对于应用更新这块是如何做的?(灰度?强制更新?分区域更新?)

  2. 混合开发:ReactNative/weex/H5/小程序/Flutter

  3. 什么情况下会导致内存泄漏

  4. 如何对Android应用进行性能分析以及优化

  5. 说一款你认为当前比较火的应用并设计(直播APP)

  6. OOM的避免异常以及解决方法

  7. 屏幕适配的处理技巧有哪些

  8. 两个Activity之间跳转时必然会执行的是哪几个方法

  1. MVC、MVP、MVVM、常见的设计模式,写出观察者模式代码

  2. TCP的三次握手和四次挥手,TCP与UDP的区别

  3. HTTP协议;HTTP 1.0与2.0的区别,HTTP报文结构

  4. 都使用过哪些框架和平台

  5. 都使用过哪些自定义控件

  6. 介绍你做过的项目

非技术问题汇总

  1. 研究的比较深的领域有哪些

  2. 对业内信息的关注渠道有哪些

  3. 最近都读了哪些书

  4. 自己最擅长的技术点,最感兴趣的技术领域和技术点

  5. 项目中使用了哪些开源库,如何避免因为引入开源库而导致的安全性和稳定性问题

  6. 实习过程中做了什么,有哪些产出

  7. 5枚硬币,2正3反,如何划分为两堆然后通过翻转让两堆中正面向上的硬币和反面向上的硬币个数相同

  8. 时针走一圈,时针和分针重合几次

  9. N*N的方格纸,里面有多少个正方形

  10. 现在下载速度很慢,试从网络协议的角度分析原因,并优化,网络的5层都可以涉及

数据结构和算法

  1. 堆和栈在内存中的区别是什么(数据结构方面以及实际实现方面)

  2. 最快的排序算法是哪个?给阿里2万多员工按年龄排序应该选择哪个算法?堆和树的区别?写出快排代码,链表逆序代码

  3. 求1000以内的水仙花数以及40亿以内的水仙花数

  4. 子串包含问题(KMP算法)写代码实现

  5. 万亿级别的两个URL文件A和B,如何求出A和B的差集C

  6. 蚁群算法和蒙特卡洛算法

  7. 写出你所知道的排序算法以及时空复杂度,稳定性

  8. 百度POI中如何试下查找最近商家功能(坐标镜像+R树)

  9. 死锁的四个必要条件

  10. 常见的编码方式,utf-8变种中文占几个字节,int型占几个字节

HR问题

  1. 上家离职的原因

  2. 讲一件你印象最深的事情

  3. 介绍一个你影响最深的项目

  4. 介绍你最热爱最擅长的专业领域

  5. 公司实习最大的收获是什么

  6. 与上级意见不一致时,你怎么办

  7. 自己的优点和缺点是什么,并举例说明

  8. 你的学习方法是什么样的?实习过程中如何学习?实习过程中遇到的最大困难是什么,以及如何解决的?

  9. 说一件最能证明你能力的事情

  10. 针对你申请的这个职位,你认为你还欠缺什么

  11. 项目中遇到的最大困难时什么,如何解决

  12. 职业规划以及个人目标,未来发展路线以及求职定位

Java 基础 (必须要完全的掌握,越熟练越好)

概述

本部分内容主要包含以下知识点,这些内容都是Java中的基础知识,对于Java的学习很有帮助.其中集合、反射、IO等都是面试常问的知识点,是必须掌握的。

  • Java集合
  • Java反射
  • Java注解
  • Java I/O
  • Java泛型
  • Cloneable
  • Serializable
  • Iterator

Java集合框架

Java集合大致可以分为Set、List、Queue和Map四种体系.

其中Set 集合:代表无序,不可重复的集合;
List:代表有序,重复的集合;
Queue:是先进先出的队列集合;
Map:代表映射关系的集合;

概述

Java集合就像一种容器,可以把多个对象(实际上是对象的引用,但习惯上都称对象)”丢进”该容器中,从Java 5 增加了泛型之后,Java集合可以记住容器中对象的数据类型,使得编码更加简洁、健壮.

  1. Java集合和数组的区别:
    a)数组长度在初始化的时候指定,意味着只能保存定长的数据。而集合可以保存数量不确定的数据,同时可以保存具有映射关系的数据(即关联数组,键值对key-value).
    b)数组的元素既可以是基本数据类型,也可以是对象.集合中只能保存对象(实际上只是保存对象的引用变量),基本数据类型的变量要转换成对应的包装类才能放入集合类中.

  2. Java集合类之间的继承关系:
    Java的集合类主要由两个接口派生而出:Collection和Map,Collection 和 Map是Java集合框架的根接口.
    Collection的派生树 如下:
    Collection的派生树
    图中后面[I]表示是接口,[C]表示是类,[AC]表示是抽象类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Collection[I]
    └── Set[I]
    | └── SortedSet[I]
    | | └── TreeSet[C]
    | └── HashSet[C]
    | | └── LinkedHashSet[C]
    | └── EnumSet[C]
    └── Queue[I]
    | └── PriorityQueue[C]
    | └── Deque[I]
    | └── ArraryDeque[C]
    | └── LinkedList[C]
    └── List[I]
    └── LinkedList[C]
    └── ArraryList[C]
    | └── AttributList[C]
    └── Vector[C]
    └── Stack[C]

    此图中,ArrayList,HashSet,LinkedList,TreeSet是我们经常会有用到的已实现的集合类.

Map 类实现用于保存具有映射关系的数据。Map保存的每项数据都是key-value对,Map里面的key是不可重复的,key用于表示集合里面的每一项数据.,这是Map和Collection最大的区别.
Map的派生树
图中后面[I]表示是接口,[C]表示是类,[AC]表示是抽象类

1
2
3
4
5
6
7
8
9
10
Map
└── SortMap
| └── TreeMap
└── WeakHashMap
└── EnumMap
└── HashMap
| └── LinkedHashMap
└── IdentityHashMapMap
└── HashTable
└── Properties

图中,HashMap、TreeMap是我们经常用到的集合类

Collection 接口

Collection是 Set、List、Queue的父接口
在接口中定义了多种方法提供给子类进行实现,以实现数据操作.
Collection.java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public abstract interface Collection<E> extends java.lang.Iterable {

public abstract int size();

public abstract boolean isEmpty();

public abstract boolean contains(java.lang.Object arg0);

public abstract java.util.Iterator<E> iterator();

public abstract java.lang.Object[] toArray();

public abstract <T> T[] toArray(T[] arg0);

public abstract boolean add(E arg0);

public abstract boolean remove(java.lang.Object arg0);

public abstract boolean containsAll(java.util.Collection<?> arg0);

public abstract boolean addAll(java.util.Collection<? extends E> arg0);

public abstract boolean removeAll(java.util.Collection<?> arg0);

public boolean removeIf(java.util.function.Predicate<? super E> arg0) {
return false;
}

public abstract boolean retainAll(java.util.Collection<?> arg0);

public abstract void clear();

public abstract boolean equals(java.lang.Object arg0);

public abstract int hashCode();

public java.util.Spliterator<E> spliterator() {
return null;
}

public java.util.stream.Stream<E> stream() {
return null;
}

public java.util.stream.Stream<E> parallelStream() {
return null;
}
}

JDK文档可以查看到每个函数的具体作用.

可以看出Collection用法有:添加元素、删除元素,返回Collection集合个数以及清空集合等.其中重点介绍iterator()方法,该方法的返回值是Iterator.

从Collection的源码看到,Iterable是Collection的父接口,Iterable的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.lang;
public abstract interface Iterable<T> {

public abstract java.util.Iterator<T> iterator();

public void forEach(java.util.function.Consumer<? super T> arg0) {
}

public java.util.Spliterator<T> spliterator() {
return null;
}
}
```
可以看到Iterable接口中有一个抽象方法iterator()
``` bash
public abstract java.util.Iterator<T> iterator();

iterator()方法的返回类型是Iterator型,我们再看看Iterator的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 // Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public abstract interface Iterator<E> {

public abstract boolean hasNext();

public abstract E next();

public void remove() {
}

public void forEachRemaining(java.util.function.Consumer<? super E> arg0) {
}
}

从JDK文档中可以看到Iterator 是对 collection 进行迭代的迭代器.主要作用是遍历Collection里面的元素.

Set 集合

Set集合和Collection集合完全相同,没有提供额外的方法,只是行为略有不同(Set不允许包含重复元素).
我们来看看Set的源码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 // Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public abstract interface Set<E> extends java.util.Collection {

public abstract int size();

public abstract boolean isEmpty();

public abstract boolean contains(java.lang.Object arg0);

public abstract java.util.Iterator<E> iterator();

public abstract java.lang.Object[] toArray();

public abstract <T> T[] toArray(T[] arg0);

public abstract boolean add(E arg0);

public abstract boolean remove(java.lang.Object arg0);

public abstract boolean containsAll(java.util.Collection<?> arg0);

public abstract boolean addAll(java.util.Collection<? extends E> arg0);

public abstract boolean retainAll(java.util.Collection<?> arg0);

public abstract boolean removeAll(java.util.Collection<?> arg0);

public abstract void clear();

public abstract boolean equals(java.lang.Object arg0);

public abstract int hashCode();

public java.util.Spliterator<E> spliterator() {
return null;
}
}

Set.java也是一个接口,从Set.java和Collection.java的代码来看,几乎一样的.

通过查看源码,我们发现最后Set集合的类,都是继承自AbstractSet类,实现了Set、Cloneable、Serializable接口
以HashSet为例,看下它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
 // Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public class HashSet<E> extends java.util.AbstractSet implements java.util.Set, java.lang.Cloneable, java.io.Serializable {

static final long serialVersionUID = -5024744406713321676L;

private transient java.util.HashMap<E,java.lang.Object> map;

private static final java.lang.Object PRESENT;

public HashSet() {
}

public HashSet(java.util.Collection<? extends E> arg0) {
}

public HashSet(int arg0, float arg1) {
}

public HashSet(int arg0) {
}

HashSet(int arg0, float arg1, boolean arg2) {
}

public java.util.Iterator<E> iterator() {
return null;
}

public int size() {
return 0;
}

public boolean isEmpty() {
return false;
}

public boolean contains(java.lang.Object arg0) {
return false;
}

public boolean add(E arg0) {
return false;
}

public boolean remove(java.lang.Object arg0) {
return false;
}

public void clear() {
}

public java.lang.Object clone() {
return null;
}

private void writeObject(java.io.ObjectOutputStream arg0) throws java.io.IOException {
}

private void readObject(java.io.ObjectInputStream arg0) throws java.io.IOException, java.lang.ClassNotFoundException {
}

public java.util.Spliterator<E> spliterator() {
return null;
}

static {} {
}
}

我们来看看AbstractSet这个抽象类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public abstract class AbstractSet<E> extends java.util.AbstractCollection implements java.util.Set {

protected AbstractSet() {
}

public boolean equals(java.lang.Object arg0) {
return false;
}

public int hashCode() {
return 0;
}

public boolean removeAll(java.util.Collection<?> arg0) {
return false;
}
}

查阅JDK api看到对AbstractSet类的解释是:此类提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。
看到这里有点懵了,

Map.java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.util;
public abstract interface Map<K,V> {

public abstract int size();

public abstract boolean isEmpty();

public abstract boolean containsKey(java.lang.Object arg0);

public abstract boolean containsValue(java.lang.Object arg0);

public abstract V get(java.lang.Object arg0);

public abstract V put(K arg0, V arg1);

public abstract V remove(java.lang.Object arg0);

public abstract void putAll(java.util.Map<? extends K,? extends V> arg0);

public abstract void clear();

public abstract java.util.Set<K> keySet();

public abstract java.util.Collection<V> values();

public abstract java.util.Set<java.util.Map.Entry<K,V>> entrySet();

public abstract boolean equals(java.lang.Object arg0);

public abstract int hashCode();

public V getOrDefault(java.lang.Object arg0, V arg1) {
return null;
}

public void forEach(java.util.function.BiConsumer<? super K,? super V> arg0) {
}

public void replaceAll(java.util.function.BiFunction<? super K,? super V,? extends V> arg0) {
}

public V putIfAbsent(K arg0, V arg1) {
return null;
}

public boolean remove(java.lang.Object arg0, java.lang.Object arg1) {
return false;
}

public boolean replace(K arg0, V arg1, V arg2) {
return false;
}

public V replace(K arg0, V arg1) {
return null;
}

public V computeIfAbsent(K arg0, java.util.function.Function<? super K,? extends V> arg1) {
return null;
}

public V computeIfPresent(K arg0, java.util.function.BiFunction<? super K,? super V,? extends V> arg1) {
return null;
}

public V compute(K arg0, java.util.function.BiFunction<? super K,? super V,? extends V> arg1) {
return null;
}

public V merge(K arg0, V arg1, java.util.function.BiFunction<? super V,? super V,? extends V> arg2) {
return null;
}
}

Java泛型

概述

  1. 引入泛型的目的
    了解引入泛型的动机,就先从语法糖开始了解

    语法糖

    语法糖(Synactic Sugar),也称糖衣语法,英国计算机学家Peter.J.Landin发明的一个术语,指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用.

    Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等.虚拟机并不支持这些语法,它们在编译阶段被还原回了简单的基础语法结构,这个过程成为解语法糖.

    泛型的目的:泛型的目的是使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现ClassCastException,即类型转换异常.

  2. 泛型初探
    JDK 1.5 才增加了泛型,并在很大程度上都是方便集合的使用,使其能够记住其元素的数据类型.
    在泛型(Generic type或Generics)出现之前,是这么写代码的:

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args)
    {
    List list = new ArrayList();
    list.add("123");
    list.add("456");

    System.out.println((String)list.get(0));
    }

    当然这是完全允许的,因为List里面的内容是Object类型的,自然任何对象类型都可以放入、都可以取出,但是这么写会有两个问题:

    1、当一个对象放入集合时,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object。
    2、运行时需要人为地强制转换类型到具体目标,实际的程序绝不会这么简单,一个不小心就会出现java.lang.ClassCastException。

    所以,泛型出现之后,上面的代码就改成了大家都熟知的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args)
    {
    List<String> list = new ArrayList<String>();
    list.add("123");
    list.add("456");

    System.out.println(list.get(0));
    }

    这就是泛型。泛型是对Java语言类型系统的一种扩展,有点类似于C++的模板,可以把类型参数看作是使用参数化类型时指定的类型的一个占位符。引入泛型,是对Java语言一个较大的功能增强,带来了很多的好处。

  3. 泛型的好处
    ①类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性。

    ②消除了代码中许多的强制类型转换,增强了代码的可读性。

    ③为较大的优化带来了可能。

泛型的使用

  1. 泛型类和泛型接口
    下面是JDK 1.5 以后,List接口,以及ArrayList类的代码片段.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //定义接口时指定了一个类型形参,该形参名为E
    public interface List<E> extends Collection<E> {
    //在该接口里,E可以作为类型使用
    public E get(int index) {}
    public void add(E e) {}
    }

    //定义类时指定了一个类型形参,该形参名为E
    public class ArrayList<E> extends AbstractList<E> implements List<E> {
    //在该类里,E可以作为类型使用
    public void set(E e) {
    .......................
    }
    }

    这就是泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。

    下面具体讲解泛型类的使用。泛型接口的使用与泛型类几乎相同,可以比对自行学习。

    泛型类

    定义一个容器类,存放键值对key-value,键值对的类型不确定,可以使用泛型来定义,分别指定为K和V。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class Container<K, V> {

    private K key;
    private V value;

    public Container(K k, V v) {
    key = k;
    value = v;
    }

    public K getkey() {
    return key;
    }

    public V getValue() {
    return value;
    }

    public void setKey() {
    this.key = key;
    }

    public void setValue() {
    this.value = value;
    }

    }

    在使用Container类时,只需要指定K,V的具体类型即可,从而创建出逻辑上不同的Container实例,用来存放不同的数据类型。

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) {
    Container<String,String> c1=new Container<String ,String>("name","hello");
    Container<String,Integer> c2=new Container<String,Integer>("age",22);
    Container<Double,Double> c3=new Container<Double,Double>(1.1,1.3);
    System.out.println(c1.getKey() + " : " + c1.getValue());
    System.out.println(c2.getKey() + " : " + c2.getValue());
    System.out.println(c3.getKey() + " : " + c3.getValue());
    }

    在JDK 1.7 增加了泛型的“菱形”语法:Java允许在构造器后不需要带完成的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。
    如下所示:

    1
    2
    Container<String,String> c1=new Container<>("name","hello");
    Container<String,Integer> c2=new Container<>("age",22);

    泛型类派生子类

    当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类派生子类,需要注意:使用这些接口、父类派生子类时不能再包含类型形参,需要传入具体的类型。
    错误的方式:

    1
    public class A extends Container<K, V>{}

    正确的方式:

    1
    public class A extends Container<Integer, String>{}

    也可以不指定具体的类型,如下:

    1
    public class A extends Container{}

    此时系统会把K,V形参当成Object类型处理。

  2. 泛型的方法
    前面在介绍泛型类和泛型接口中提到,可以在泛型类、泛型接口的方法中,把泛型中声明的类型形参当成普通类型使用。 如下面的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Container<K, V>
    {
    ........................
    public K getkey() {
    return key;
    }
    public void setKey() {
    this.key = key;
    }
    ....................
    }

    但在另外一些情况下,在类、接口中没有使用泛型时,定义方法时想定义类型形参,就会使用泛型方法。如下方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Main{
    public static <T> void out(T t){
    System.out.println(t);
    }
    public static void main(String[] args){
    out("hansheng");
    out(123);
    }
    }

    所谓泛型方法,就是在声明方法时定义一个或多个类型形参。 泛型方法的用法格式如下:

    1
    2
    3
    修饰符<T, S> 返回值类型 方法名(形参列表){
    方法体

    注意: 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Demo{  
    public <T> T fun(T t){ // 可以接收任意类型的数据
    return t ; // 直接把参数返回
    }
    };
    public class GenericsDemo26{
    public static void main(String args[]){
    Demo d = new Demo() ; // 实例化Demo对象
    String str = d.fun("汤姆") ; // 传递字符串
    int i = d.fun(30) ; // 传递数字,自动装箱
    System.out.println(str) ; // 输出内容
    System.out.println(i) ; // 输出内容
    }
    };

    当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。

  3. 泛型构造器
    正如泛型方法允许在方法签名中声明类型形参一样,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
    和使用普通泛型方法一样没区别,一种是显式指定泛型参数,另一种是隐式推断,如果是显式指定则以显式指定的类型参数为准,如果传入的参数的类型和指定的类型实参不符,将会编译报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Person {
    public <T> Person(T t) {
    System.out.println(t);
    }

    }
    public static void main(String[] args){
    //隐式
    new Person(22);
    //显示
    new<String> Person("hello");
    }

    这里唯一需要特殊注明的就是,如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:
    因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。
    以下面这个例子为代表

    1
    2
    3
    4
    5
    6
    public class Person<E> {
    public <T> Person(T t) {
    System.out.println(t);
    }

    }

    这种用法:

    1
    Person<String> a = new <Integer>Person<>(15);

    这种语法不允许,会直接编译报错!

类型通配符

顾名思义就是匹配任意类型的类型实参.
类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被成为通配符,它的元素类型可以匹配任何类型。

1
2
3
4
5
public void test(List<?> c){
for(int i =0;i<c.size();i++){
System.out.println(c.get(i));
}
}

现在可以传入任何类型的List来调用test()方法,程序依然可以访问集合c中的元素,其类型是Object。

1
2
3
List<?> c = new ArrayList<String>();
//编译器报错
c.add(new Object());

但是并不能把元素加入到其中。因为程序无法确定c集合中元素的类型,所以不能向其添加对象。
下面就该引入带限通配符,来确定集合元素中的类型。

带限通配符
单来讲,使用通配符的目的是来限制泛型的类型参数的类型,使其满足某种条件,固定为某些类。
主要分为两类即:上限通配符下限通配符

  1. 上限通配符
    如果想限制使用泛型类别时,只能用某个特定类型或者是其子类型才能实例化该类型时,可以在定义类型时,使用extends关键字指定这个类型必须是继承某个类,或者实现某个接口,也可以是这个类或接口本身

    表示集合中的所有元素都是Shape类型或者其子类

    1
    List<? extends Shape>

    这就是所谓的上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    例如:

    1
    2
    //Circle是其子类
    List<? extends Shape> list = new ArrayList<Circle>();

    这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。然后进行其他操作。

  2. 下限通配符
    如果想限制使用泛型类别时,只能用某个特定类型或者是其父类型才能实例化该类型时,可以在定义类型时,使用super关键字指定这个类型必须是是某个类的父类,或者是某个接口的父接口,也可以是这个类或接口本身。

    表示集合中的所有元素都是Circle类型或者其父类

    1
    List <? super Circle>

    这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    例如:

    1
    2
    //Shape是其父类
    List<? super Circle> list = new ArrayList<Shape>();

类型擦除

1
2
3
Class c1=new ArrayList<Integer>().getClass();
Class c2=new ArrayList<String>().getClass();
System.out.println(c1==c2);

程序输出:

1
true

这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。

Java 注解

元数据

要理解注解(Annotation)作用,就要先理解Java中的元数据的概念。

  1. 元数据概念
    元数据是关于数据的数据,在编程语言上下文中,元数据是添加到程序元素如方法、字段、类和包上的额外信息。对数据进行说明描述的数据。

  2. 元数据的作用
    一般来说,元数据可以用于创建文档(根据程序元素上的注释创建文档),跟踪代码中的依赖性(可声明方法是重载,依赖父类的方法),执行编译时检查(可声明是否编译期监测),代码分析。
    如下:
    1) 编写文档: 通过代码里标识的元数据生成文档
    2) 代码分析: 通过代码里标识的元数据对代码进行分析
    3)编译检查: 通过代码里标识的元数据让编译器能实现基本的编译检查

  3. Java平台元数据
    注解Annotation就是Java平台的元数据,是J2SE5.0新增加的功能,该机制允许在Java代码中添加自定义注释,并允许通过反射(reflection),以编程方式访问元数据注释。通过提供为程序元素(类、方法等)附加额外数据的标准方法,元数据功能具有简化和改进许多应用程序开发领域的潜在能力,其中包括配置管理、框架实现和代码生成。

注解(Annotation)

  1. 注解(Annotation)的概念
    注解是在JDK1.5之后新增加的一个新特性,注解的引入意义很大,有很多非常有名的框架,比如Hibernate、Spring等框架中都大量使用注解。注解作为程序的元数据嵌入到程序。注解可以被解析工具或者编译工具解析。

    关于注解(Annotation)的作用,其实就是上述元数据的作用。

    注意:Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据。Annotation不影响程序代码的执行,无论增加、删除Annotation,代码始终如一的执行。如果希望让程序中的Annotation起一定的作用,只有通过解析工具或者编译工具对Annotation中的信息进行解析和处理.

  2. 内建注解
    Java提供了多种内建的注解,下面接下几个比较常用的注解:@Override、@Deprecated、@SuppresWarnings以及@FunctionallInterface者四个注解。内建注解主要实现呢了元数据的第二个作用:编译检查

    @Override
    用途:用于告知编译器,我们需要覆写超类的当前方法。如果某个方法带有该注解但并没有覆写超类相应的方法,则编译器会生成一条错误信息。如果父类没有这个要覆写的方法,则编译器也会生成一条错误信息。

    @Override可适用元素为方法,仅仅保留在Java源文件中。

    @Deprecated
    用途:使用这个注解,用于告知编译器吗,某一程序元素(比如方法,成员变量)不建议使用了(即过时了).
    例如:
    Person类中的info()方法使用@Deprecated表示该方法过时了。

    1
    2
    3
    4
    5
    6
    public class Person {
    @Deprecated
    public void info(){

    }
    }

    调用info()方法会编译器会出现警告,告知该方法已过时。

    1
    2
    3
    4
    5
    6
    public class Main{
    public static void main(String[] args){
    Person person = nre Person();
    person.info();//info函数会有一个删除线
    }
    }

    注解类型分析:@Deprecated可适合用于除注解类型声明之外的所有元素,保留时长为运行时。

@SuppressWarnings
用途:用于告知编译器忽略特定的警告信息,例在泛型中使用原生数据类型,编译器会发出警告,当使用该注解后,则不会发出警告。
注解类型分析: @SuppressWarnings可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。

该注解有方法value(),可支持多个字符串参数,用户指定忽略哪种警告,例如:

1
@SupressWarning(value={"uncheck","deprecation"})

SuppressWarning参数

@FunctionalInterface
用途:用户告知编译器,检查这个接口,保证该接口是函数式接口,即只能包含一个抽象方法,否则就会编译出错。

注解类型分析: @FunctionalInterface可适合用于注解类型声明,保留时长为运行时。

  1. 元Annotation
    JDK除了在java.lang提供了上述内建注解外,还在java.lang.annotation包下提供了6个Meta Annotation(元Annotataion),其中有5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用户定义Java 8 新增的可重复注解。

    我们先介绍其中4个常用的修饰其他Annotation的元Annotation。在此之前,我们先了解如何自定义Annotation。

    当一个接口直接继承java.lang.annotation.Annotation接口时,仍是接口,而并非注解。要想自定义注解类型,只能通过@interface关键字的方式,其实通过该方式会隐含地继承.Annotation接口。

    @Documented

    @Documented用户指定被该元Annotation修饰的Annotation类将会被javadoc工具提取成文档,如果定义Annotation类时使用了@Documented修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该Annotation说明。
    例如:

    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
    public @interface Deprecated {
    }

    定义@Deprecated时使用了@Documented,则任何元素使用@Deprecated修饰时,在生成API文档时,将会包含@Deprecated的说明
    以下是String的一个过时的构造方法:

    1
    2
    @Deprecated
    public String(byte[] ascii,int hibyte,int offset, int count)

    该注解实现了元数据的第一个功能:编写文档

    @Inherited
    @Retention:表示该注解类型的注解保留的时长。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。关于保留策略(RetentionPolicy)是枚举类型,共定义3种保留策略,如下表:
    Retention

    @Target
    @Target:表示该注解类型的所适用的程序元素类型。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。如果存在指定的@Target元注解,则编译器强制实施相应的使用限制。关于程序元素(ElementType)是枚举类型,共定义8种程序元素,如下表:
    Target

自定义注解(Annotation)

创建自定义注解,与创建接口有几分相似,但注解需要以@开头。

1
2
3
4
5
6
7
8
9
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotataion{
String name();
String website() default "hello";
int revision() default 1;
}

自定义注解中定义成员变量的规则:
其定义是以无形参的方法形式来声明的。即:
注解方法不带参数,比如name(),website();
注解方法返回值类型:基本类型、String、Enums、Annotation以及前面这些类型的数组类型
注解方法可有默认值,比如default “hello”,默认website=”hello”

当然注解中也可以不存在成员变量,在使用解析注解进行操作时,仅以是否包含该注解来进行操作。当注解中有成员变量时,若没有默认值,需要在使用注解时,指定成员变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
public class AnnotationDemo {
@MyAnnotataion(name="lvr", website="hello", revision=1)
public static void main(String[] args) {
System.out.println("I am main method");
}

@SuppressWarnings({ "unchecked", "deprecation" })
@MyAnnotataion(name="lvr", website="hello", revision=2)
public void demo(){
System.out.println("I am demo method");
}
}

由于该注解的保留策略为RetentionPolicy.RUNTIME,故可在运行期通过反射机制来使用,否则无法通过反射机制来获取。这时候注解实现的就是元数据的第二个作用:代码分析
下面来具体介绍如何通过反射机制来进行注解解析。

注解解析

接下来,通过反射技术来解析自定义注解。关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement,该接口主要有如下几个实现类:Class,Constructor,Field,Method,Package。除此之外,该接口定义了注释相关的几个核心方法,如下:
Reflect

因此,当获取了某个类的Class对象,然后获取其Field,Method等对象,通过上述4个方法提取其中的注解,然后获得注解的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AnnotationParser {
public static void main(String[] args) throws SecurityException, ClassNotFoundException {
String clazz = "com.lvr.annotation.AnnotationDemo";
Method[] demoMethod = AnnotationParser.class
.getClassLoader().loadClass(clazz).getMethods();

for (Method method : demoMethod) {
if (method.isAnnotationPresent(MyAnnotataion.class)) {
MyAnnotataion annotationInfo = method.getAnnotation(MyAnnotataion.class);
System.out.println("method: "+ method);
System.out.println("name= "+ annotationInfo.name() +
" , website= "+ annotationInfo.website()
+ " , revision= "+annotationInfo.revision());
}
}
}
}

上仅是一个示例,其实可以根据拿到的注解信息做更多有意义的事。

Java 反射

概念

  1. Java反射机制的定义
    Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  2. Java反射机制的功能
    a).在运行时判断任意一个对象所属的类。
    b).在运行时构造任意一个类的对象。
    c).在运行时判断任意一个类所具有的成员变量和方法。
    d).在运行时调用任意一个对象的方法。
    e).生成动态代理。

  3. Java 反射机制的应用场景
    a).逆向代码 例如反编译
    b).与注解相结合的框架 例如Retrofit
    c).单纯的反射机制应用框架 例如EventBus
    d).动态生成类框架 例如Gson

通过Java反射查看类信息

一、 获得Class对象
每个类被加载之后,系统就会为该类生成一个对应的Class对象。通过该Class对象就可以访问到JVM中的这个类。
在Java程序中获得Class对象通常有如下三种方式:

  1. 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定名(必须添加完整包名)。
  2. 调用某个类的class属性来获取该类对应的Class对象。
  3. 调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法。

    1
    2
    3
    4
    5
    6
    7
    //第一种方式 通过Class类的静态方法——forName()来实现
    class1 = Class.forName("com.lvr.reflection.Person");
    //第二种方式 通过类的class属性
    class1 = Person.class;
    //第三种方式 通过对象getClass方法
    Person person = new Person();
    Class<?> class1 = person.getClass();

    二、获取class对象的属性、方法、构造函数等

  4. 获取class对象的成员变量

    1
    2
    3
    4
    Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性
    Field[] publicFields = class1.getFields();//获取class对象的public属性
    Field ageField = class1.getDeclaredField("age");//获取class指定属性
    Field desField = class1.getField("des");//获取class指定的public属性
  5. 获取class对象的方法

    1
    2
    3
    4
    Method[] methods = class1.getDeclaredMethods();//获取class对象的所有声明方法
    Method[] allMethods = class1.getMethods();//获取class对象的所有public方法 包括父类的方法
    Method method = class1.getMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的public方法
    Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的方法
  6. 获取class对象的构造函数

    1
    2
    3
    4
    Constructor<?>[] allConstructors = class1.getDeclaredConstructors();//获取class对象的所有声明构造函数
    Constructor<?>[] publicConstructors = class1.getConstructors();//获取class对象public构造函数
    Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//获取指定声明构造函数
    Constructor publicConstructor = class1.getConstructor(String.class);//获取指定声明的public构造函数
  7. 其他方法

    1
    2
    3
    4
    Annotation[] annotations = (Annotation[]) class1.getAnnotations();//获取class对象的所有注解
    Annotation annotation = (Annotation) class1.getAnnotation(Deprecated.class);//获取class对象指定注解
    Type genericSuperclass = class1.getGenericSuperclass();//获取class对象的直接超类的 Type
    Type[] interfaceTypes = class1.getGenericInterfaces();//获取class对象的所有接口的type集合

    三、 获取class对象的信息
    比较多

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型
    boolean isArray = class1.isArray();//判断是否是集合类
    boolean isAnnotation = class1.isAnnotation();//判断是否是注解类
    boolean isInterface = class1.isInterface();//判断是否是接口类
    boolean isEnum = class1.isEnum();//判断是否是枚举类
    boolean isAnonymousClass = class1.isAnonymousClass();//判断是否是匿名内部类
    boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);//判断是否被某个注解类修饰
    String className = class1.getName();//获取class名字 包含包名路径
    Package aPackage = class1.getPackage();//获取class的包信息
    String simpleName = class1.getSimpleName();//获取class类名
    int modifiers = class1.getModifiers();//获取class访问权限
    Class<?>[] declaredClasses = class1.getDeclaredClasses();//内部类
    Class<?> declaringClass = class1.getDeclaringClass();//外部类

通过Java反射生成并操作对象

一、生成类的实例对象

  1. 使用Class对象的newInstance()方法来创建该Class对象对应类的实例。这种方式要求该Class对象的对应类有默认构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例。

  2. 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。

    1
    2
    3
    4
    5
    //第一种方式 Class对象调用newInstance()方法生成
    Object obj = class1.newInstance();
    //第二种方式 对象获得对应的Constructor对象,再通过该Constructor对象的newInstance()方法生成
    Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//获取指定声明构造函数
    obj = constructor.newInstance("hello");

二、调用类的方法

  1. 通过Class对象的getMethods()方法或者getMethod()方法获得指定方法,返回Method数组或对象。

  2. 调用Method对象中的Object invoke(Object obj, Object… args)方法。第一个参数对应调用该方法的实例对象,第二个参数对应该方法的参数。

    1
    2
    3
    4
    5
    6
    // 生成新的对象:用newInstance()方法
    Object obj = class1.newInstance();
    //首先需要获得与该方法对应的Method对象
    Method method = class1.getDeclaredMethod("setAge", int.class);
    //调用指定的函数并传递参数
    method.invoke(obj, 28);

当通过Method的invoke()方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的private方法,则可以先调用Method对象的如下方法。
setAccessible(boolean flag):将Method对象的acessible设置为指定的布尔值。值为true,指示该Method在使用时应该取消Java语言的访问权限检查;值为false,则知识该Method在使用时要实施Java语言的访问权限检查。

三、访问成员变量值

  1. 通过Class对象的getFields()方法或者getField()方法获得指定方法,返回Field数组或对象。

  2. Field提供了两组方法来读取或设置成员变量的值:
    getXXX(Object obj):获取obj对象的该成员变量的值。此处的XXX对应8种基本类型。如果该成员变量的类型是引用类型,则取消get后面的XXX。
    setXXX(Object obj,XXX val):将obj对象的该成员变量设置成val值。

    1
    2
    3
    4
    5
    6
    7
    8
    //生成新的对象:用newInstance()方法 
    Object obj = class1.newInstance();
    //获取age成员变量
    Field field = class1.getField("age");
    //将obj对象的age的值设置为10
    field.setInt(obj, 10);
    //获取obj对象的age的值
    field.getInt(obj);

代理模式

定义:给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。

  1. 代理模式的理解
    代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。现实世界的代理人被授权执行当事人的一些事宜,无需当事人出面,从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。
    在软件设计中,使用代理模式的意图也很多,比如因为安全原因需要屏蔽客户端直接访问真实对象,或者在远程调用中需要使用代理类处理远程方法调用的技术细节,也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。

  2. 代理模式的参与者
    代理模式的角色分四种:
    Proxy
    主题接口:Subject 是委托对象和代理对象都共同实现的接口,即代理类的所实现的行为接口。Request() 是委托对象和代理对象共同拥有的方法。
    目标对象:ReaSubject 是原对象,也就是被代理的对象。
    代理对象:Proxy 是代理对象,用来封装真是主题类的代理类。
    客户端 :使用代理类和主题接口完成一些工作。

  3. 代理模式的分类
    代理的实现分为:

    静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。
    动态代理:代理类是在运行时生成的。也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。

  4. 代理模式的实现思路
    a).代理对象和目标对象均实现同一个行为接口。
    b).代理类和目标类分别具体实现接口逻辑。
    c).在代理类的构造函数中实例化一个目标对象。
    d).在代理类中调用目标对象的行为接口。
    e).客户端想要调用目标对象的行为接口,只能通过代理类来操作。

  5. 静态代理模式的简单实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class ProxyDemo {
    public static void main(String args[]){
    RealSubject subject = new RealSubject();
    Proxy p = new Proxy(subject);
    p.request();
    }
    }

    interface Subject{
    void request();
    }

    class RealSubject implements Subject{
    public void request(){
    System.out.println("request");
    }
    }

    class Proxy implements Subject{
    private Subject subject;
    public Proxy(Subject subject){
    this.subject = subject;
    }
    public void request(){
    System.out.println("PreProcess");
    subject.request();
    System.out.println("PostProcess");
    }
    }

    目标对象(RealSubject )以及代理对象(Proxy)都实现了主题接口(Subject)。在代理对象(Proxy)中,通过构造函数传入目标对象(RealSubject ),然后重写主题接口(Subject)的request()方法,在该方法中调用目标对象(RealSubject )的request()方法,并可以添加一些额外的处理工作在目标对象(RealSubject )的request()方法的前后。

    代理模式的好处:
    假如有这样的需求,要在某些模块方法调用前后加上一些统一的前后处理操作,比如在添加购物车、修改订单等操作前后统一加上登陆验证与日志记录处理,该怎样实现?首先想到最简单的就是直接修改源码,在对应模块的对应方法前后添加操作。如果模块很多,你会发现,修改源码不仅非常麻烦、难以维护,而且会使代码显得十分臃肿。

    这时候就轮到代理模式上场了,它可以在被调用方法前后加上自己的操作,而不需要更改被调用类的源码,大大地降低了模块之间的耦合性,体现了极大的优势。

    静态代理比较简单,上面的简单实例就是静态代理的应用方式,下面介绍本篇文章的主题:动态代理。

Java反射机制与动态代理

动态代理的思路和上述思路一致,下面主要讲解如何实现。

  1. 动态代理介绍
    动态代理是指在运行时动态生成代理类。即,代理类的字节码将在运行时生成并载入当前代理的 ClassLoader。与静态处理类相比,动态类有诸多好处。

    ①不需要为(RealSubject )写一个形式上完全一样的封装类,假如主题接口(Subject)中的方法很多,为每一个接口写一个代理方法也很麻烦。如果接口有变动,则目标对象和代理类都要修改,不利于系统维护;
    ②使用一些动态代理的生成方法甚至可以在运行时制定代理类的执行逻辑,从而大大提升系统的灵活性。

  2. 动态代理涉及的主要类
    主要涉及两个类,这两个类都是java.lang.reflect包下的类,内部主要通过反射来实现的。

    java.lang.reflect.Proxy:这是生成代理类的主类,通过 Proxy 类生成的代理类都继承了 Proxy 类。
    Proxy提供了用户创建动态代理类和代理对象的静态方法,它是所有动态代理类的父类。

    java.lang.reflect.InvocationHandler:这里称他为”调用处理器”,它是一个接口。只有一个invoke方法。当调用动态代理类中的方法时,将会直接转接到执行自定义的InvocationHandler中的invoke()方法。即我们动态生成的代理类需要完成的具体内容需要自己定义一个类,而这个类必须实现 InvocationHandler 接口,通过重写invoke()方法来执行具体内容。
    InvocationHandler.java代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package java.lang.reflect;

    /**
    * {@code InvocationHandler} is the interface implemented by
    * the <i>invocation handler</i> of a proxy instance.
    *
    * <p>Each proxy instance has an associated invocation handler.
    * When a method is invoked on a proxy instance, the method
    * invocation is encoded and dispatched to the {@code invoke}
    * method of its invocation handler.
    *
    * @author Peter Jones
    * @see Proxy
    * @since 1.3
    */
    public interface InvocationHandler {

    /**
    * Processes a method invocation on a proxy instance and returns
    * the result. This method will be invoked on an invocation handler
    * when a method is invoked on a proxy instance that it is
    * associated with.
    *
    * @param proxy the proxy instance that the method was invoked on
    *
    * @param method the {@code Method} instance corresponding to
    * the interface method invoked on the proxy instance. The declaring
    * class of the {@code Method} object will be the interface that
    * the method was declared in, which may be a superinterface of the
    * proxy interface that the proxy class inherits the method through.
    *
    * @param args an array of objects containing the values of the
    * arguments passed in the method invocation on the proxy instance,
    * or {@code null} if interface method takes no arguments.
    * Arguments of primitive types are wrapped in instances of the
    * appropriate primitive wrapper class, such as
    * {@code java.lang.Integer} or {@code java.lang.Boolean}.
    *
    * @return the value to return from the method invocation on the
    * proxy instance. If the declared return type of the interface
    * method is a primitive type, then the value returned by
    * this method must be an instance of the corresponding primitive
    * wrapper class; otherwise, it must be a type assignable to the
    * declared return type. If the value returned by this method is
    * {@code null} and the interface method's return type is
    * primitive, then a {@code NullPointerException} will be
    * thrown by the method invocation on the proxy instance. If the
    * value returned by this method is otherwise not compatible with
    * the interface method's declared return type as described above,
    * a {@code ClassCastException} will be thrown by the method
    * invocation on the proxy instance.
    *
    * @throws Throwable the exception to throw from the method
    * invocation on the proxy instance. The exception's type must be
    * assignable either to any of the exception types declared in the
    * {@code throws} clause of the interface method or to the
    * unchecked exception types {@code java.lang.RuntimeException}
    * or {@code java.lang.Error}. If a checked exception is
    * thrown by this method that is not assignable to any of the
    * exception types declared in the {@code throws} clause of
    * the interface method, then an
    * {@link UndeclaredThrowableException} containing the
    * exception that was thrown by this method will be thrown by the
    * method invocation on the proxy instance.
    *
    * @see UndeclaredThrowableException
    */
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;
    }

    Proxy提供了如下两个方法来创建动态代理类和动态代理实例。

    static Class<?> getProxyClass(ClassLoader loader, Class<?>… interfaces) 返回代理类的java.lang.Class对象。第一个参数是类加载器对象(即哪个类加载器来加载这个代理类到 JVM 的方法区),第二个参数是接口(表明你这个代理类需要实现哪些接口),第三个参数是调用处理器类实例(指定代理类中具体要干什么),该代理类将实现interfaces所指定的所有接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 返回代理类实例。参数与上述方法一致。

对应上述两种方法创建动态代理对象的方式:

1
2
3
4
5
6
7
8
//创建一个InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(.args..);
//使用Proxy生成一个动态代理类
Class proxyClass = Proxy.getProxyClass(RealSubject.class.getClassLoader(),RealSubject.class.getInterfaces(), handler);
//获取proxyClass类中一个带InvocationHandler参数的构造器
Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
//调用constructor的newInstance方法来创建动态实例
RealSubject real = (RealSubject)constructor.newInstance(handler);

1
2
3
4
//创建一个InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(.args..);
//使用Proxy直接生成一个动态代理对象
RealSubject real =Proxy.newProxyInstance(RealSubject.class.getClassLoader(),RealSubject.class.getInterfaces(), handler);

newProxyInstance这个方法实际上做了两件事:第一,创建了一个新的类【代理类】,这个类实现了Class[] interfaces中的所有接口,并通过你指定的ClassLoader将生成的类的字 节码加载到JVM中,创建Class对象;第二,以你传入的InvocationHandler作为参数创建一个代理类的实例并返回。

Proxy 类还有一些静态方法,比如:

1
2
3
InvocationHandler getInvocationHandler(Object proxy):获得代理对象对应的调用处理器对象。

Class getProxyClass(ClassLoader loader, Class[] interfaces):根据类加载器和实现的接口获得代理类。

InvocationHandler 接口中有方法:

1
invoke(Object proxy, Method method, Object[] args)

这个函数是在代理对象调用任何一个方法时都会调用的,方法不同会导致第二个参数method不同,第一个参数是代理对象(表示哪个代理对象调用了method方法),第二个参数是 Method 对象(表示哪个方法被调用了),第三个参数是指定调用方法的参数。

  1. 动态代理模式的简单实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    public class DynamicProxyDemo {
    public static void main(String[] args) {
    //1.创建目标对象
    RealSubject realSubject = new RealSubject();
    //2.创建调用处理器对象
    ProxyHandler handler = new ProxyHandler(realSubject);
    //3.动态生成代理对象
    Subject proxySubject = (Subject)Proxy.newProxyInstance(RealSubject.class.getClassLoader(),
    RealSubject.class.getInterfaces(), handler);
    //4.通过代理对象调用方法
    proxySubject.request();
    }
    }

    /**
    * 主题接口
    */
    interface Subject{
    void request();
    }

    /**
    * 目标对象类
    */
    class RealSubject implements Subject{
    public void request(){
    System.out.println("====RealSubject Request====");
    }
    }
    /**
    * 代理类的调用处理器
    */
    class ProxyHandler implements InvocationHandler{
    private Subject subject;
    public ProxyHandler(Subject subject){
    this.subject = subject;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    //定义预处理的工作,当然你也可以根据 method 的不同进行不同的预处理工作
    System.out.println("====before====");
    //调用RealSubject中的方法
    Object result = method.invoke(subject, args);
    System.out.println("====after====");
    return result;
    }
    }

    可以看到,我们通过newProxyInstance就产生了一个Subject 的实例,即代理类的实例,然后就可以通过Subject .request(),就会调用InvocationHandler中的invoke()方法,传入方法Method对象,以及调用方法的参数,通过Method.invoke调用RealSubject中的方法的request()方法。同时可以在InvocationHandler中的invoke()方法加入其他执行逻辑。

泛型和Class类

从JDK 1.5 后,Java中引入泛型机制,Class类也增加了泛型功能,从而允许使用泛型来限制Class类,例如:String.class的类型实际上是Class。如果Class对应的类暂时未知,则使用Class<?>(?是通配符)。通过反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换。

泛型的好处众多,最主要的一点就是避免类型转换,防止出现ClassCastException,即类型转换异常。以下面程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ObjectFactory {
public static Object getInstance(String name){
try {
//创建指定类对应的Class对象
Class cls = Class.forName(name);
//返回使用该Class对象创建的实例
return cls.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}

}

上面程序是个工厂类,通过指定的字符串创建Class对象并创建一个类的实例对象返回。但是这个对象的类型是Object对象,取出实例后需要强制类型转换。
如下例:

1
Date date = (Date) ObjectFactory.getInstance("java.util.Date");

又或者如下:

1
String string = (String) ObjectFactory.getInstance("java.util.Date");

上面代码在编译时不会有任何问题,但是运行时将抛出ClassCastException异常,因为程序试图将一个Date对象转换成String对象。
但是泛型的出现后,就可以避免这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
public class ObjectFactory {
public static <T> T getInstance(Class<T> cls) {
try {
// 返回使用该Class对象创建的实例
return cls.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}

}

在上面程序的getInstance()方法中传入一个Class参数,这是一个泛型化的Class对象,调用该Class对象的newInstance()方法将返回一个T对象。

1
String instance = ObjectFactory.getInstance(String.class);

通过传入String.class便知道T代表String,所以返回的对象是String类型的,避免强制类型转换。
当然Class类引入泛型的好处不止这一点,在以后的实际应用中会更加能体会到。

使用反射来获取泛型信息

通过指定类对应的 Class 对象,可以获得该类里包含的所有 Field,不管该 Field 是使用 private 修饰,还是使用 public 修饰。获得了 Field 对象后,就可以很容易地获得该 Field 的数据类型,即使用如下代码即可获得指定 Field 的类型。

1
2
// 获取 Field 对象 f 的类型
Class<?> a = f.getType();

但这种方式只对普通类型的 Field 有效。如果该 Field 的类型是有泛型限制的类型,如 Map<String, Integer> 类型,则不能准确地得到该 Field 的泛型参数。
为了获得指定 Field 的泛型类型,应先使用如下方法来获取指定 Field 的类型。

1
2
// 获得 Field 实例的泛型类型
Type type = f.getGenericType();

然后将 Type 对象强制类型转换为 ParameterizedType 对象,ParameterizedType 代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType 类提供了如下两个方法。
getRawType():返回没有泛型信息的原始类型。
getActualTypeArguments():返回泛型参数的类型。
下面是一个获取泛型类型的完整程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class GenericTest
{
private Map<String , Integer> score;
public static void main(String[] args)
throws Exception
{
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
// 直接使用getType()取出Field类型只对普通类型的Field有效
Class<?> a = f.getType();
// 下面将看到仅输出java.util.Map
System.out.println("score的类型是:" + a);
// 获得Field实例f的泛型类型
Type gType = f.getGenericType();
// 如果gType类型是ParameterizedType对象
if(gType instanceof ParameterizedType)
{
// 强制类型转换
ParameterizedType pType = (ParameterizedType)gType;
// 获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:" + rType);
// 取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型类型是:");
for (int i = 0; i < tArgs.length; i++)
{
System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
}
}
else
{
System.out.println("获取泛型类型出错!");
}
}
}

输出结果:

1
2
3
4
5
score 的类型是: interface java.util.Map
原始类型是: interface java.util.Map
泛型类型是:
第 0 个泛型类型是: class java.lang.String
第 1 个泛型类型是:class java.lang.Integer

从上面的运行结果可以看出,直接使用 Field 的 getType() 方法只能获取普通类型的 Field 的数据类型:对于增加了泛型参数的类型的 Field,应该使用 getGenericType() 方法来取得其类型。

Type 也是 java.lang.reflect 包下的一个接口,该接口代表所有类型的公共高级接口,Class 是 Type 接口的实现类。Type 包括原始类型、参数化类型、数组类型、类型变量和基本类型等。

Java I/O

字符和字节

在Java中有输入、输出两种I/O流,每种输入、输出流又分为字节流和字符流。关于字节,我们在学习8大基本数据类型中都有了解,每个字节(byte)有8bit组成,每种数据类型又有几个字节组成等,关于字符,我们知道代表一个汉字或者英文字母。

但是字节和字符之间的关系是怎么样的呢?

Java采用Unicode编码,2个字节来表示一个字符,这点与C语言不同,C语言采用ASCII码,在大多数系统中,一个字符通常占一个字节,但是在0~127整数之间的字符映射,Unicode向下兼容ASCII。而Java采用unicode来表示字符,一个中文或英文字符的unicode编码都占2个字节。但如果采用其他编码方式,一个字符占用的字节数则各不相同。可能有点晕,举个例子解释下。

例如:Java中的String类是按照unicode进行编码的,当使用String(byte[] bytes, String encoding)构造字符串时,encoding所指的是bytes中的数据是按照那种方式编码的,而不是最后产生的String是什么编码方式,换句话说,是让系统把bytes中的数据由encoding编码方式转换成unicode编码。如果不指明,bytes的编码方式将由jdk根据操作系统决定。

getBytes(String charsetName)使用指定的编码方式将此String编码为 byte 序列,并将结果存储到一个新的 byte 数组中。如果不指定将使用操作系统默认的编码方式,我的电脑默认的是GBK编码。

1
2
3
4
5
6
7
8
9
10
public class Hel {  
public static void main(String[] args){
String str = "你好hello";
int byte_len = str.getBytes().length;
int len = str.length();
System.out.println("字节长度为:" + byte_len);
System.out.println("字符长度为:" + len);
System.out.println("系统默认编码方式:" + System.getProperty("file.encoding"));
}
}

输出结果:

1
2
3
字节长度为:9
字符长度为:7
系统默认编码方式:GBK

这是因为:在 GB 2312 编码或 GBK 编码中,一个英文字母字符存储需要1个字节,一个汉字字符存储需要2个字节。 在UTF-8编码中,一个英文字母字符存储需要1个字节,一个汉字字符储存需要3到4个字节。在UTF-16编码中,一个英文字母字符存储需要2个字节,一个汉字字符储存需要3到4个字节(Unicode扩展区的一些汉字存储需要4个字节)。在UTF-32编码中,世界上任何字符的存储都需要4个字节。

简单来讲,一个字符表示一个汉字或英文字母,具体字符与字节之间的大小比例视编码情况而定。有时候读取的数据是乱码,就是因为编码方式不一致,需要进行转换,然后再按照unicode进行编码。

File类

File类是java.io包下代表与平台无关的文件和目录,也就是说,如果希望在程序中操作文件或者目录,都可以通过File类来完成。

  1. 构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    //构造函数File(String pathname)
    File f1 =new File("c:\\abc\\1.txt");
    //File(String parent,String child)
    File f2 =new File("c:\\abc","2.txt");
    //File(File parent,String child)
    File f3 =new File("c:"+File.separator+"abc");//separator 跨平台分隔符
    File f4 =new File(f3,"3.txt");
    System.out.println(f1);//c:\abc\1.txt

    路径分隔符:
    windows: “/“ “” 都可以
    linux/unix: “/“
    注意:如果windows选择用””做分割符的话,那么请记得替换成”\”,因为Java中””代表转义字符
    所以推荐都使用”/“,也可以直接使用代码File.separator,表示跨平台分隔符。
    路径:
    相对路径:
    ./表示当前路径
    ../表示上一级路径
    其中当前路径:默认情况下,java.io 包中的类总是根据当前用户目录来分析相对路径名。此目录由系统属性 user.dir 指定,通常是 Java 虚拟机的调用目录。”

    绝对路径:
    绝对路径名是完整的路径名,不需要任何其他信息就可以定位自身表示的文件

  2. 创建与删除方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //如果文件存在返回false,否则返回true并且创建文件 
    boolean createNewFile();
    //创建一个File对象所对应的目录,成功返回true,否则false。且File对象必须为路径而不是文件。只会创建最后一级目录,如果上级目录不存在就抛异常。
    boolean mkdir();
    //创建一个File对象所对应的目录,成功返回true,否则false。且File对象必须为路径而不是文件。创建多级目录,创建路径中所有不存在的目录
    boolean mkdirs() ;
    //如果文件存在返回true并且删除文件,否则返回false
    boolean delete();
    //在虚拟机终止时,删除File对象所表示的文件或目录。
    void deleteOnExit();
  3. 判断方法

    1
    2
    3
    4
    5
    6
    7
    8
    boolean canExecute()    ;//判断文件是否可执行
    boolean canRead();//判断文件是否可读
    boolean canWrite();//判断文件是否可写
    boolean exists();//判断文件是否存在
    boolean isDirectory();//判断是否是目录
    boolean isFile();//判断是否是文件
    boolean isHidden();//判断是否是隐藏文件或隐藏目录
    boolean isAbsolute();//判断是否是绝对路径 文件不存在也能判断
  4. 获取方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    String getName();//返回文件或者是目录的名称
    String getPath();//返回路径
    String getAbsolutePath();//返回绝对路径
    String getParent();//返回父目录,如果没有父目录则返回null
    long lastModified();//返回最后一次修改的时间
    long length();//返回文件的长度
    File[] listRoots();// 列出所有的根目录(Window中就是所有系统的盘符)
    String[] list() ;//返回一个字符串数组,给定路径下的文件或目录名称字符串
    String[] list(FilenameFilter filter);//返回满足过滤器要求的一个字符串数组
    File[] listFiles();//返回一个文件对象数组,给定路径下文件或目录
    File[] listFiles(FilenameFilter filter);//返回满足过滤器要求的一个文件对象数组

    其中包含了一个重要的接口FileNameFilter,该接口是个文件过滤器,包含了一个accept(File dir,String name)方法,该方法依次对指定File的所有子目录或者文件进行迭代,按照指定条件,进行过滤,过滤出满足条件的所有文件。

1
2
3
4
5
6
7
// 文件过滤
File[] files = file.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String filename) {
return filename.endsWith(".mp3");
}
});

file目录下的所有子文件如果满足后缀是.mp3的条件的文件都会被过滤出来。

IO流的概念

Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源抽象表述为”流”。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内存,文件,网络或程序等。

IO流的分类:

  1. 输入流和输出流
    根据数据流向不同分为:输入流和输出流。

    输入流:只能从中读取数据,而不能向其写入数据。
    输出流:只能向其写入数据,而不能从中读取数据。

  2. 字节流和字符流
    字节流和字符流和用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同。
    字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。字节流和字符流的区别:
    (1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
    (2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

    只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

  3. 节点流和处理流
    按照流的角色来分,可以分为节点流和处理流。
    可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被成为低级流。
    处理流是对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能,处理流也被称为高级流。

    1
    2
    3
    4
    //节点流,直接传入的参数是IO设备
    FileInputStream fis = new FileInputStream("test.txt");
    //处理流,直接传入的参数是流对象
    BufferedInputStream bis = new BufferedInputStream(fis);

    当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。
    实际上,Java使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。

IO流的四大基类

根据流的流向以及操作的数据单元不同,将流分为了四种类型,每种类型对应一种抽象基类。这四种抽象基类分别为:InputStream,Reader,OutputStream以及Writer。四种基类下,对应不同的实现类,具有不同的特性。在这些实现类中,又可以分为节点流和处理流。下面就是整个由着四大基类支撑下,整个IO流的框架图。
IO流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
I/O流
└── 字符流
| └── Reader
| | └── BufferReader
| | └── InputStreamReader - FileReader
| | └── StringReader
| | └── PipedReader
| | └── ByteArrayReader
| | └── FilterReader - PushBackReader
| └── Writer
| └── BufferWriter
| └── OutputStreamWriter - FileWriter
| └── StringWriter
| └── PipedWriter
| └── CharWriter
| └── FilterWriter
└── 字节流
└── InputStream
| └── FileInputStream
| └── FilterInputStream
| | └── BufferInputStream
| | └── DataInputStream
| | └── PushBackInputStream
| └── ObjectInputStream
| └── PipedInputStream
| └── SequenceInputStream
| └── StringBufferInputStream
| └── ByteArrayInputStream
└── OutputStream
└── FileOutputStream
└── FilterOutputStream
| └── BufferOutputStream
| └── DataOutputStream
| └── PrintOutputStream
└── ObjectOutputStream
└── PipedOutputStream
└── ByteArrayOutputStream

InputStream,Reader,OutputStream以及Writer,这四大抽象基类,本身并不能创建实例来执行输入/输出,但它们将成为所有输入/输出流的模版,所以它们的方法是所有输入/输出流都可以使用的方法。类似于集合中的Collection接口。

  1. InputStream
    InputStream 是所有的输入字节流的父类,它是一个抽象类,主要包含三个方法:

    1
    2
    3
    4
    5
    6
    //读取一个字节并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。 
    int read() ;
    //读取一系列字节并存储到一个数组buffer,返回实际读取的字节数,如果读取前已到输入流的末尾返回-1。
    int read(byte[] buffer) ;
    //读取length个字节并存储到一个字节数组buffer,从off位置开始存,最多len, 返回实际读取的字节数,如果读取前以到输入流的末尾返回-1。
    int read(byte[] buffer, int off, int len) ;
  2. Reader
    Reader 是所有的输入字符流的父类,它是一个抽象类,主要包含三个方法:

    1
    2
    3
    4
    5
    6
    //读取一个字符并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。 
    int read() ;
    //读取一系列字符并存储到一个数组buffer,返回实际读取的字符数,如果读取前已到输入流的末尾返回-1。
    int read(char[] cbuf) ;
    //读取length个字符,并存储到一个数组buffer,从off位置开始存,最多读取len,返回实际读取的字符数,如果读取前以到输入流的末尾返回-1。
    int read(char[] cbuf, int off, int len)

    对比InputStream和Reader所提供的方法,就不难发现两个基类的功能基本一样的,只不过读取的数据单元不同。

    在执行完流操作后,要调用close()方法来关系输入流,因为程序里打开的IO资源不属于内存资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。

    除此之外,InputStream和Reader还支持如下方法来移动流中的指针位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //在此输入流中标记当前的位置
    //readlimit - 在标记位置失效前可以读取字节的最大限制。
    void mark(int readlimit)
    // 测试此输入流是否支持 mark 方法
    boolean markSupported()
    // 跳过和丢弃此输入流中数据的 n 个字节/字符
    long skip(long n)
    //将此流重新定位到最后一次对此输入流调用 mark 方法时的位置
    void reset()
  3. OutputStream
    OutputStream 是所有的输出字节流的父类,它是一个抽象类,主要包含如下四个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    //向输出流中写入一个字节数据,该字节数据为参数b的低8位。 
    void write(int b) ;
    //将一个字节类型的数组中的数据写入输出流。
    void write(byte[] b);
    //将一个字节类型的数组中的从指定位置(off)开始的,len个字节写入到输出流。
    void write(byte[] b, int off, int len);
    //将输出流中缓冲的数据全部写出到目的地。
    void flush();
  4. Writer
    Writer 是所有的输出字符流的父类,它是一个抽象类,主要包含如下六个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //向输出流中写入一个字符数据,该字节数据为参数b的低16位。 
    void write(int c);
    //将一个字符类型的数组中的数据写入输出流,
    void write(char[] cbuf)
    //将一个字符类型的数组中的从指定位置(offset)开始的,length个字符写入到输出流。
    void write(char[] cbuf, int offset, int length);
    //将一个字符串中的字符写入到输出流。
    void write(String string);
    //将一个字符串从offset开始的length个字符写入到输出流。
    void write(String string, int offset, int length);
    //将输出流中缓冲的数据全部写出到目的地。
    void flush();

    可以看出,Writer比OutputStream多出两个方法,主要是支持写入字符和字符串类型的数据。

    使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,还能将输出流缓冲区的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush()方法)

    以上内容就是整个IO流的框架介绍。

RandomAccessFile

RandomAccessFile概述

RandomAccessFile既可以读取文件内容,也可以向文件输出数据。同时,RandomAccessFile支持“随机访问”的方式,程序快可以直接跳转到文件的任意地方来读写数据。

由于RandomAccessFile可以自由访问文件的任意位置,所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。

与OutputStream、Writer等输出流不同的是,RandomAccessFile允许自由定义文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容。如果程序需要向已存在的文件后追加内容,则应该使用RandomAccessFile。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。

RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。

RandomAccessFile中的方法

  1. RandomAccessFile的构造函数
    RandomAccessFile类有两个构造函数,其实这两个构造函数基本相同,只不过是指定文件的形式不同——一个需要使用String参数来指定文件名,一个使用File参数来指定文件本身。除此之外,创建RandomAccessFile对象时还需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,一共有4种模式。

    “r”: 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
    “rw”: 打开以便读取和写入。
    “rws”: 打开以便读取和写入。相对于 “rw”,”rws” 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
    “rwd” : 打开以便读取和写入,相对于 “rw”,”rwd” 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。

  2. RandomAccessFile的重要方法

    RandomAccessFile既可以读文件,也可以写文件,所以类似于InputStream的read()方法,以及类似于OutputStream的write()方法,RandomAccessFile都具备。除此之外,RandomAccessFile具备两个特有的方法,来支持其随机访问的特性。

    RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件指针记录位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会后移n个字节。除此之外,RandomAccessFile还可以自由移动该记录指针。下面就是RandomAccessFile具有的两个特殊方法,来操作记录指针,实现随机访问:

    long getFilePointer( ):返回文件记录指针的当前位置
    void seek(long pos ):将文件指针定位到pos位置

RandomAccessFile的使用

利用RandomAccessFile实现文件的多线程下载,即多线程下载一个文件时,将文件分成几块,每块用不同的线程进行下载。下面是一个利用多线程在写文件时的例子,其中预先分配文件所需要的空间,然后在所分配的空间中进行分块,然后写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/** 
* 测试利用多线程进行文件的写操作
*/
public class Test {

public static void main(String[] args) throws Exception {
// 预分配文件所占的磁盘空间,磁盘中会创建一个指定大小的文件
RandomAccessFile raf = new RandomAccessFile("D://abc.txt", "rw");
raf.setLength(1024*1024); // 预分配 1M 的文件空间
raf.close();

// 所要写入的文件内容
String s1 = "第一个字符串";
String s2 = "第二个字符串";
String s3 = "第三个字符串";
String s4 = "第四个字符串";
String s5 = "第五个字符串";

// 利用多线程同时写入一个文件
new FileWriteThread(1024*1,s1.getBytes()).start(); // 从文件的1024字节之后开始写入数据
new FileWriteThread(1024*2,s2.getBytes()).start(); // 从文件的2048字节之后开始写入数据
new FileWriteThread(1024*3,s3.getBytes()).start(); // 从文件的3072字节之后开始写入数据
new FileWriteThread(1024*4,s4.getBytes()).start(); // 从文件的4096字节之后开始写入数据
new FileWriteThread(1024*5,s5.getBytes()).start(); // 从文件的5120字节之后开始写入数据
}

// 利用线程在文件的指定位置写入指定数据
static class FileWriteThread extends Thread{
private int skip;
private byte[] content;

public FileWriteThread(int skip,byte[] content){
this.skip = skip;
this.content = content;
}

public void run(){
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile("D://abc.txt", "rw");
raf.seek(skip);
raf.write(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
raf.close();
} catch (Exception e) {
}
}
}
}

}

当RandomAccessFile向指定文件中插入内容时,将会覆盖掉原有内容。如果不想覆盖掉,则需要将原有内容先读取出来,然后先把插入内容插入后再把原有内容追加到插入内容后。

Java NIO

NIO 概述

Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java1.4开始),Java NIO提供了与标准IO不同的IO工作方式。

所以Java NIO是一种新式的IO标准,与之间的普通IO的工作方式不同。标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道也类似。

由上面的定义就说明NIO是一种新型的IO,但NIO不仅仅就是等于Non-blocking IO(非阻塞IO),NIO中有实现非阻塞IO的具体类,但不代表NIO就是Non-blocking IO(非阻塞IO)。

Java NIO 由以下几个核心部分组成:

  • Buffer
  • Channel
  • Selector
    传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。

Buffer的使用

利用Buffer读写数据,通常遵循四个步骤:

  • 把数据写入buffer;
  • 调用flip;
  • 从Buffer中读取数据;
  • 调用buffer.clear()

未完待续

Java 异常详解

Java异常概述

Java异常是Java提供的一种识别及响应错误的一致性机制。

Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出。

Java异常机制用到的几个关键字:try、catch、finally、throw、throws。

• try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

• catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。

• finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

• throw – 用于抛出异常。

• throws – 用在方法签名中,用于声明该方法可能抛出的异常。

下面通过几个示例对这几个关键字进行简单了解。
示例一: 了解try和catch基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo1 {

public static void main(String[] args) {
try {
int i = 10/0;
System.out.println("i="+i);
} catch (ArithmeticException e) {
System.out.println("Caught Exception");
System.out.println("e.getMessage(): " + e.getMessage());
System.out.println("e.toString(): " + e.toString());
System.out.println("e.printStackTrace():");
e.printStackTrace();
}
}
}

运行结果:

1
2
3
4
5
6
Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
at Demo1.main(Demo1.java:6)

结果说明:在try语句块中有除数为0的操作,该操作会抛出java.lang.ArithmeticException异常。通过catch,对该异常进行捕获。

观察结果我们发现,并没有执行System.out.println(“i=”+i)。这说明try语句块发生异常之后,try语句块中的剩余内容就不会再被执行了。

示例二: 了解finally的基本用法,在”示例一”的基础上,我们添加finally语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo2 {

public static void main(String[] args) {
try {
int i = 10/0;
System.out.println("i="+i);
} catch (ArithmeticException e) {
System.out.println("Caught Exception");
System.out.println("e.getMessage(): " + e.getMessage());
System.out.println("e.toString(): " + e.toString());
System.out.println("e.printStackTrace():");
e.printStackTrace();
} finally {
System.out.println("run finally");
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
at Demo2.main(Demo2.java:6)
run finally
```
结果说明:最终执行了finally语句块。

示例三: 了解throws和throw的基本用法
**throws**是**用于声明抛出的异常**,而**throw**是**用于抛出异常**。
``` bash
class MyException extends Exception {
public MyException() {}
public MyException(String msg) {
super(msg);
}
}

public class Demo3 {

public static void main(String[] args) {
try {
test();
} catch (MyException e) {
System.out.println("Catch My Exception");
e.printStackTrace();
}
}
public static void test() throws MyException{
try {
int i = 10/0;
System.out.println("i="+i);
} catch (ArithmeticException e) {
throw new MyException("This is MyException");
}
}
}

运行结果:

1
2
3
4
Catch My Exception
MyException: This is MyException
at Demo3.test(Demo3.java:24)
at Demo3.main(Demo3.java:13)

运行结果说明:MyException是继承于Exception的子类。test()的try语句块中产生ArithmeticException异常(除数为0),并在catch中捕获该异常;接着抛出MyException异常。main()方法对test()中抛出的MyException进行捕获处理。

Java异常框架

未完待续

Java抽象类和接口的区别

未完待续

Java深拷贝和浅拷贝

未完待续

Java中transient关键字

未完待续

Java中finally和return的执行顺序

未完代码

Java 8 新特性

Java 8 源码

JDk 1.8 Source Code

Java 并发

本部分内容是关于Java并发的一些知识总结,既是学习的难点,同时也是面试中几乎必问的知识点。

面试中可能会问的一些问题:
• 创建线程的方式
• Synchronized/ReentrantLock
• 生产者/消费者模式
• volatile关键字
• 乐观锁/悲观锁
• 死锁
• 了解的并发集合

Java创建线程的三种方式

Java线程池

死锁

Synchronize/Reentrantlock

生产者消费者模式


Android基础

主要复习Android的一些个基础知识点

  • 四大组件
  • 事件分发机制
  • 消息机制
  • binder机制
  • 线程和进程

Activity全方位解析

先从Activity的功能上复习

MyQuestion:

  1. 能否从Framework的层面来分析一下,Activity这样设计的原理和机制是什么?
  1. 如何从Framework代码中看出Activity的设计逻辑

Activity生命周期

  1. Activity生命周期
    典型的生命周期
    onCreate -> onRestart -> onStart -> onResume -> onPause -> onStop -> onDestory
    onCreate:加载布局资源,初始化Activity所需要的数据,
    onRestart:从不可见变为可见
    onStart:表示Activity正在被启动,此时Activity已经出现,但是还没有出现在前台,无法与用户交互,这个时候可以理解为Activity已经显示出来,但是我们看不到。
    onResume:表示Activity已经可见,且出现在前台
    onPause:表示Activity正在停止,但是仍可见,正常情况下,紧接着onStop就会被调用。特殊情况下,如果这个时候快速的返回当前Activity,那么onResume就会被调用(极端情况)
    onStop:表示Activity即将停止,不可见,位于后台,可以做稍微重量级的回收工作,同样不能太耗时。
    onDestory:表示Activity即将被销毁,这是Activity的最后一个回调,可以做一些回收工作和最终的资源回收。、

  2. 特殊情况下的生命周期
    a) 横竖屏切换
    横竖屏切换的过程中,会发生Activity被销毁和重建的过程。
    onSaveInstanceState和onRestoreInstanceState

    onSaveInstanceState在onStop之前被调用
    onRestoreInstanceState在onStart之后调用

    b)资源不足导致优先级低的Activity被杀死

Activity的启动模式

从Framework中Activity相关代码可以看到,Activity的管理是通过ActivityStack任务栈的形式来管理。
Activity的LaunchMode主要有四种:

  1. Standard 标准模式:每次启动Activity,都会new一个新的Activity放在栈顶
  2. SingleTop 栈顶复用:栈顶存在,则直接使用,不用new
  3. SingleTask 栈内复用
  4. SingleInstance 单例模式

    特殊情况:前台栈和后台栈交互

Activity的Flag

FLAG_ACTIVITY_NEW_TASK

FLAG_ACTIVITY_SINGLE_TOP

FLAG_ACTIVITY_CLEAR_TOP

Service 全方位解析

Service是ANdroid中实现程序后台运行的解决方案,它非常适用于执行那些不需要和用户交互而且还要求长期运行的任务。Servcie默认并不会运行在子线程中,它也不运行在一个独立的进程中,它同样执行在UI线程中,因此,不要在Service中执行耗时的操作,除非你在Service中创建子线程来完成耗时操作。

MyQuestion:

  1. 在Android Framework代码中,Service的机制是什么?
  1. Service种类
    按运行类型分类:

    区别                                    应用
    

    前台服务 会在通知栏显示onGoing的Notification 服务被终止时,通知栏Notification也会消失,如:音乐播放服务
    后台服务 默认的服务即为后台服务,不会在通知栏显示 服务被终止时,用户看不到,如:天气更新、日期同步、邮件同步

    按使用方式分类:
    类别 区别
    startServcie启动的服务 主要用于启动一个服务咨询后台任务,不进行通讯,停止服务,使用stopService
    bindService启动的服务 可以进行进程通讯.停止服务,使用unbindService
    同时使用StartServcie和bindService启动的服务 停止服务使用stopService和unbindServcie

  2. Service生命周期
    onCreate -> onStartCommand -> onDestory
    onCreate -> onBind -> onUnbind -> onDestory
    onCreate:系统在Service第一次创建时执行此方法,如果service已经运行,此方法不会调用
    onStartCommand:每次客户端调用startService方法启动该service,都会回调该方法
    onBind:当调用bindService时,执行该方法,一次调用

BroadcastReceiver全方位解析

BroadcastReceiver广播接收器,使用了观察者模式来实现。
模型中有三个角色:
a)消息订阅者 –广播接收者
b)消息发布者 –广播发送者
c)消息中心(AMS,即ActivityManagerService)

原理描述:
i) 广播接受者通过Binder机制在AMS注册
ii)广播发送者通过Binder机制向AMS发送广播
iii)AMS根据广播发送者要求,在已注册列表中,寻求合适的广播接收者

  • 寻找依据:IntenFilter/Permission
    iv)AMS将广播发送到合适的广播接收者相应的消息循环队列中
    v)广播接收者通过消息循环 拿到此广播,并回调onReceive()

    特别注意:广播发送者和广播接收者执行的是 异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底何时才能接收到。

    参考文章

    My Question:
    需要拿出AMS的源码来详细的分析原理

ContenProvider全方位解析

  1. 作用:进程间 数据交互和共享,跨进程通讯
    ContentProvider只是中间者角色,真正存储&操作数据的数据源还是原来存储数据的方式(数据库、文件、xml、网络)

  2. 原理:ContentProvider 采用的是Android中Binder机制来实现

  3. 使用

  • URL 统一资源标识符
  • MIME 数据类型
  • ContentProvider类的 使用方法和数据组织方式
  • 辅助工具类:a)ContentResolver类 b)ContentUris 类 c)UriMatcher类 d) ContentObserver类

    3.1 统一资源标识符
    Uniform Resource Identifier
    作用:唯一标识ContentProvider 其中的数据
    具体使用,分两类,系统预制和自定义

    自定义URI = content://com.carson.provider/User/1
    主题(Schema):URI的前缀,由Android规定
    授权信息(Authority):唯一标识符,一般是包名
    表名(Path):指向数据库中的某个表
    记录(ID):表中的某个记录(若无指定,则返回全部记录)

    3.2 MIME数据类型

    3.3 ContentProvider 类

    参考文章

Fragment 详解

  1. 什么是Fragment
    简单来说,就是显示在Activity中的Activity

  2. Fragment的生命周期
    由于Fragment是依附于Activity存在的,因此,它的生命周期受到Activity的生命周期的影响
    onCreate(){ onAttach() -> onCreate() -> onCreateView() -> onActivityCreated() }
    onStart(){ onStart() }
    onResume(){ onResume() }
    onPause(){ onPause() }
    onStop(){ onStop() }
    onDestory(){ onDestoryView() -> onDestory() -> onDetach() }

    PS: 注意:除了onCreateView,其他的所有方法如果重写了,必须调用父类对于该方法的实现

  3. Fragment的使用方式
    静态使用Fragment

    动态使用Fragment

  4. 什么是Fragment的回退栈?
    Fragment的回退栈是用来保存每一次Fragment事务发生的变化,如果你将Fragment任务添加到回退栈,当用户点击回退按钮时,将看到上一次保存的Fragment。一旦Fragment完全从回退栈弹出,用户再次点击后退键,则退出当前Activity

    FragmentTransaction.addToBackStack(String)

  5. Fragment和Activity之间的通信

  6. Fragment与Activity通信的优化

  7. 如何处理运行时配置发生变化

    参考文章

Android消息机制

Android中的消息机制,即Handler.

  1. 消息机制的模型
    消息机制主要包含:MessageQueue/Handler/Looper/Message四个模块,现在来一一介绍

    Message: 需要传递的消息,可以装载数据
    MessageQueue: 消息队列,它的内部实现并不是用的队列,实际是通过一个单链表的数据结构来维护消息列表,因为单链表在插入和删除上比较有优势。主要功能是向消息池投递消息
    (MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next). MessageQueue
    Handler: 消息辅助类,主要功能是向信息池发送各种消息事件(Handler.sendMessage)和取走处理相应消息事件(Handler.handleMessage).
    Looper:不断循环执行(Looper.loop),从MessageQueue中读取消息,按分发机制将消息分发给目标处理.

  2. 消息机制的架构

  1. 消息机制原理
    frameworks/base/core/java/android/os/MessageQueue.java
    frameworks/base/core/java/android/os/Message.java
    frameworks/base/core/java/android/os/Message.aidl
    frameworks/base/core/java/android/os/Looper.java
    frameworks/base/core/java/android/os/Handler.java

    参考文章

Android事件分发机制

  1. 事件在哪些对象间进行传递
    Activity、ViewGroup、View,事件产生后,传递顺序:Activity > ViewGroup > View

  2. 事件分发的本质,就是Activity/ViewGroup/View三者的事件分发
    参考资料2中有对源码进行分析

    参考资料[1]:Myblog中的 《Android编程下Touch事件的分发和消费机制》

    参考资料2

  3. onTouch和onTouchEvent的区别

  4. Touch事件的后续(Move、up)事件的层级传递

AsyncTask详解

  1. AsyncTask是一个抽象类,是由Android封装的一个轻量级异步类,它可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程并在主线程中更新UI
    AsyncTask的内部封装了两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler).

  2. AsyncTask的使用和源码分析见参考资料1

  3. AsyncTask使用不当的后果
    a) 生命周期
    AsyncTask 不与任何组件绑定生命周期,所以在Activity或者Fragment中创建执行AsyncTask时,最好在Activity/Fragment的onDestory调用cancel方法

    b)内存泄漏
    如果AsyncTask被声明为Activity的非静态的内部类,那么AsyncTask会保留一个对创建了AsyncTask的Activity的引用.
    如果Activity已经被销毁,AsyncTask的后台线程还在执行,将继续保留这个引用,导致Activity无法被回收,引起内存泄漏

    c)结果丢失
    屏幕旋转或者Activity在后台被系统杀掉等情况导致的Activity重建,之前运行的AsyncTask会持有一个之前Activity的引用,这个引用已经无效,这是调用onPOSTExecute()再去更新界面将不再生效.

    参考资料1

HandlerThread详解

在Android中执行耗时的操作都需要另外开启子线程来执行,执行完线程以后会自动销毁,如果项目中经常需要执行耗时操作,如果经常开启线程,接着又销毁线程,无疑会消耗性能,解决方法是:
a)使用线程池
b)使用HandlerThread

HandlerThread的代码以及在Framework中封装好:/frameworks/base/core/java/android/os/HandlerThread.java,采用的是Handler和Looper来实现

HandlerThread的使用,见参考资料1

参考资料1

IntentServcie详解

IntentService是Android里面的一个封装类,继承自四大组件之一的Service.
作用:处理异步请求,实现多线程.

LruCache详解

Android中缓存策略,一般来说,缓存策略主要包含缓存的添加、获取和删除者三类操作.
不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的,当缓存满了之后,再向其添加缓存,这时就需要删除一些旧的缓存并添加新的缓存.

LRU(Least Recently Used)是最近最少使用算法,采用LRU算法的缓存有两种:LruCache和DisLruCache,分别是内存缓存和硬盘缓存.

  1. LruCache的使用
    LruCache是Android3.1提供的一个缓存类.

  2. LruCache源码
    看LruCache源码,发现在Android的framework层的代码,大部分实现都用到Java的基础知识.所以还是要好好掌握下Java的基础知识.

Window、Activity、DecorView以及ViewRoot之间的关系

View的测量、布局以及绘制原理

Android虚拟机以及编译过程

Android进程间通信方式

Android Bitmap压缩策略

Android 动画总结

Android 进程优先级

Android Contex 详解

Android进阶

Android 多线程断点续传

Android全局异常处理

Android MVP模式详解

Android Binder机制以及AIDL使用

Android Parcelable和Serializable的区别

一个APP从启动到主页面显示经历了哪些过程?

Android 性能优化总结

Android 内存泄漏总结

Android 布局优化之include/merge/ViewStub的使用

Android 权限处理

Android热修复原理

Android 插件化入门指南

VirtualAPK解析

Android 推送技术解析

Android APK安装过程

PopupWindow和Dialog区别

Java 虚拟机

《深入理解Java虚拟机》–JVM高级特性与最佳实践
周志明-机械工业出版社

关于Java虚拟机,重点考察以下三个方面:

  • 内存区域/内存模型
  • 类加载机制
  • 垃圾收集算法/收集器

MyQuestion:

  1. JVM 开源么? JVM是包含在JDK里面的么?

  2. Dalvik对JVM 做了哪些改进?

  3. ART又对Dalvik做了哪些改进?

虚拟机相关的关键字:
JVM:
HotSpot:
JIT
AOT:
Heap(堆):
Stack(栈):

对象的创建、内存布局和访问定位

对象的创建

  1. 虚拟机遇到一个New指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;

  2. 检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有,那必须先执行相应的类的加载过程;

  3. 在类加载检查功能通过后,为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。

对象的内存布局

分为三个区域: 对象头,实例数据,对齐填充。

  1. 对象头
    包含两部分信息,第一部分:对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。着不放数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称它为”Mark Word”.

    第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据 。

  2. 实例数据
    是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

  3. 对齐填充
    对齐填充不是必然存在的。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍。因此,当对象实例数据部分没有对齐是,就需要通过对齐补充来补全了。

对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象

目前主流的访问方式有使用句柄和直接使用指针两种。

  1. 句柄访问
    Java堆中会划分出一块内存来作为句柄池,reference中存储的事就对象的句柄地址,而句柄中包含了对实例数据与类型数据的各自具体的地址信息。
    句柄访问

  2. 直接指针访问
    reference中存储的直接就是对象地址
    直接指针访问

Java内存区域

  1. 方法区(公有)

  2. 堆(公有)

  3. 虚拟机栈(线程私有)

  4. 本地方法栈(线程私有)

  5. 程序计数器(线程私有)

Java内存模型

Java内存模型的目的: 屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

了解Java内存模型之前需要了解:主内存和工作内存。

Java内存模型规定了所有的变量都存储在主内存中,每个线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

Java类加载机制

  1. 定义: 把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

    在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点来实现的。

  2. 类的生命周期: 加载、验证、准备、解析、初始化、使用和卸载。其中验证,准备,解析3个部分统称为链接。
    Loading -> Verification -> Preparation -> Resolution -> Initalization -> Using -> Unloading

类加载器详解

通过上述的了解,我们已经知道了类加载机制的大概流程以及各个部分的功能,其中加载部分的功能是将类的class文件读入内存,并为之创建一个java.lang.Class对象。这部分功能就是由类加载器来实现的。

  1. 类加载器分类
    不同的类加载器负责加载不同的类。主要分为两类:
    启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对Hotspot),负责将存放在<JAVA_HOME>/lib目录或者-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的核心类

    其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:

    扩展类加载器(Extension ClassLoader):负责将存放在<JAVA_HOME>/lib/ext目录或者java.ext.dirs系统变量指定的路径中的类库加载到内存中,即负责加载Java扩展的核心类之外的类

    应用程序类加载器(Application ClassLoader):

  2. 双亲委派模型
    双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有父加载器在它的搜索范围内没有找到所需的类时,即无法完成该加载,子加载才会尝试自己去加载该类。

  1. 双亲委派模型的代码实现
    ClassLoader中loadClass方法实现了双亲委派模型

JVM中垃圾收集算法

  1. 标记-清除算法
    最基础的收集算法是”标记-清除”(Mark-Sweep)算法,分为标记和清除两个阶段
    a)首先标记出所需要回收的对象
    b)在标记完成之后,统一回收所有被标记的对象

    不足:
    效率问题: 标记和清除两个过程的效率都不高
    空间问题:标记清除后产生大量的不连续的内存碎片,空间碎片太多,可能会导致分配大对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作

  2. 复制算法
    目的:为了解决效率问题

    将可用内存按照容量大小划分为大小相等的两块,每次只使用其中的一块。当一块内存使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。

    缺点:将内存缩小为了原来的一半

    现代的商业虚拟机都是采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中对象98%对象是”朝生夕死”,所以不需要按照1:1来划分内存空间,而是将内存分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。HotSpot虚拟机中默认Eden和Survivor的大小比例是8:1

  3. 标记-整理算法
    复制算法在对象存活率较高的时候,就要进行较多的复制操作,效率就会变低。

    标记过程和标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

  4. 分代收集算法
    一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

    对新生代,采用复制算法

    对老年代,采用标记整理算法或者标记清除算法

##垃圾收集器详解

JVM怎么判断对象是否已死

  1. 引用计数法
    给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1.任何时刻计数器为0的对象就是不可能被在使用的。

    主流的JVM里面没有选用引用计数器算法来管理内存,其中最主要的原因是它很难解决对象间的互相循环引用问题。

  2. 可达性分析算法

  3. 判断对象是否存活与”引用”相关
    Strong Reference 强引用
    Soft Reference 软引用
    Weak Reference 弱引用
    Phantom Reference 虚引用
    引用强度依次减弱

参考资料

Android校招面试指南