目录
Android Activity 的生命周期
Fragment 的生命周期
Activity 四大启动模式
Android 中的四大组件
Android 的线程通信方式
Android 中的布局类型、布局优化方法及常用标签
Android 中解决多线程冲突的方法
Android 图片缓存与加载的实现方式
Collection 和 Collections 的使用区别
HashMap 的底层结构实现
ConcurrentHashMap 的底层结构实现
HashMap 与 Hashtable 的区别
ArrayList 与 LinkedList 的区别
线程安全的队列有哪些
StringBuffer 和 StringBuilder 的区别
单例模式的具体实现
双检锁的具体含义,以及两次检查的意义分别是什么
对 Java 多线程的理解
线程的实现方式有哪些
线程之间的通信方式
守护线程的介绍
死锁的概念及避免方法
volatile 和 synchronized 的详解及两者区别
Java 的 GC 算法
Java 的引用类型,弱引用的作用、引用队列
TCP 和 UDP 的区别
OSI 五层结构
HTTP 报文的组成
HTTP 请求获取数据的两种方式
粘包分包产生的原因及解决方法
在下载过程中如何显示进度条?若下载操作耗时且数据大小不确定,该如何处理
MVP 模式与 MVC 模式的对比
观察者模式的应用场景
装饰者模式的原理
数据库左连接和右连接的区别及其关键字
算法与数据结构
给定银行卡前缀(如前 4 位或前 5 位),如何查找银行卡号所属银行?该查找操作的时间复杂度是多少(java 代码完整实现)
JRE、JDK、JVM 的区别
开发过程中遇到的异常类型及处理方法
如何实现 ImageLoader
Activity 作为 Android 中与用户交互的核心组件,其生命周期由系统全程管理,理解各阶段的状态变化对开发稳定的应用至关重要。
当 Activity 首次创建时,会依次执行 onCreate
、onStart
和 onResume
方法。onCreate
是生命周期的起点,在此可完成布局加载、数据初始化等关键操作,如调用 setContentView
设置界面布局;onStart
方法执行时,Activity 已对用户可见但尚未获取焦点;onResume
则表示 Activity 已处于前台并能响应用户操作,此时是开启动画或注册传感器监听的合适时机。
当用户切换至其他应用或打开新 Activity 时,当前 Activity 会进入暂停状态,依次触发 onPause
方法。该方法需完成轻量级的状态保存,如暂停动画、释放非关键资源,但要避免耗时操作,否则会影响新 Activity 的显示速度。若新 Activity 完全覆盖当前 Activity,还会执行 onStop
方法,此时 Activity 不再可见,可进行更耗时的资源释放,如取消网络请求,但需注意若用户快速返回,onStop
可能尚未执行完毕。
当用户返回该 Activity 时,会按 onRestart
、onStart
、onResume
的顺序恢复运行。onRestart
仅在 Activity 从停止状态重新启动时调用,可用于重新加载数据。而当 Activity 被系统销毁时,onDestroy
方法会被调用,此处必须释放所有资源,如解绑服务、关闭数据库连接等,以避免内存泄漏。
此外,当设备配置发生变化(如旋转屏幕),Activity 会经历销毁重建的过程,依次执行 onPause
、onStop
、onDestroy
,然后重新创建并执行 onCreate
、onStart
、onResume
。为避免数据丢失,可通过 onSaveInstanceState
保存临时状态,系统会在重建时将数据传入 onCreate
或 onRestoreInstanceState
。理解这些生命周期方法的调用时机,能帮助开发者更好地管理组件状态和资源释放。
Fragment 作为可嵌入 Activity 的组件,其生命周期既依赖于宿主 Activity,又有自身独特的阶段变化,与 Activity 生命周期的配合需细致处理。
在 Fragment Activity 关联时,首先会调用 onAttach
方法,此时 Fragment 已与 Activity 建立联系,可通过 getActivity()
获取宿主引用,但视图尚未创建。接着执行 onCreate
,用于初始化 Fragment 的核心数据,如接收参数 Bundle,其作用类似 Activity 的 onCreate
,但不涉及视图构建。
视图创建阶段是 Fragment 特有的流程,onCreateView
方法需返回 Fragment 的根视图,在此可通过 LayoutInflater
inflater.inflate(R.layout.fragment_layout, container, false) 加载布局,其中 container 参数为父容器,false 表示不立即添加到容器。视图创建完成后,
onViewCreated` 方法被调用,可在此对视图进行初始化,如设置点击事件,此时视图已可用但宿主 Activity 可能尚未完全启动。
当宿主 Activity 进入 onStart
和 onResume
时,Fragment 相应地执行 onStart
和 onResume
,此时 Fragment 对用户可见并可交互。而当 Activity 暂停或停止时,Fragment 依次触发 onPause
、onStop
,与 Activity 的对应方法类似,用于释放临时资源。
Fragment 与 Activity 生命周期的关键差异在于视图的销毁阶段。当 Fragment 被移除但仍与 Activity 关联时,会先调用 onDestroyView
,销毁视图但保留 Fragment 实例,适用于需要保留数据但释放视图资源的场景,如 ViewPager 中的 Fragment 切换。当 Fragment 彻底与 Activity 分离时,执行 onDestroy
销毁实例,最后通过 onDetach
解除与 Activity 的关联。
理解 Fragment 生命周期与 Activity 的嵌套关系尤为重要。例如,当 Activity 旋转屏幕时,Fragment 会经历 onDestroyView
而非 onDestroy
,因此需在 onSaveInstanceState
中保存视图相关数据,而在 onCreateView
中恢复。这种精细的生命周期管理,使得 Fragment 能更灵活地在 Activity 中复用和切换,同时避免资源泄漏。
Activity 的启动模式决定了系统如何管理其在任务栈中的实例创建与复用,合理使用不同模式可优化应用性能并实现特定交互逻辑。
standard 模式是系统默认的启动方式,每次启动 Activity 都会在当前任务栈中创建新实例,无论该实例是否已存在。例如,在 Activity A 中多次启动 Activity A,任务栈中会依次压入多个 A 实例,返回时按后进先出顺序退出。这种模式适用于大多数普通页面,如详情页,每次打开都需要独立的实例状态。
singleTop 模式则会检查任务栈顶是否已存在目标 Activity 实例。若存在,则直接复用该实例并调用其 onNewIntent
方法传递参数,不再创建新实例;若不存在,则新建实例。例如,消息通知页采用该模式,当用户多次点击通知时,若通知页已在栈顶,则直接刷新内容而非创建多个页面。此模式能避免在栈顶重复创建相同 Activity,减少内存占用。
singleTask 模式具有更强的实例管理能力,它会在整个系统范围内查找是否存在包含该 Activity 的任务栈。若存在,则清空该任务栈中位于此 Activity 之上的所有实例,并将其置于栈顶;若不存在,则新建任务栈并创建实例。典型应用是应用的主界面,当用户从其他应用返回主界面时,系统会清除主界面之上的所有 Activity,确保主界面以干净的状态呈现,实现 “一键回主” 的效果。
singleInstance 模式最为特殊,它会创建一个独立的任务栈,该 Activity 在此栈中是唯一的实例,且其他 Activity 无法进入此栈。这种模式适用于需要全局唯一且独立运行的场景,如电话接听界面,无论从何处启动,都始终在单独的任务栈中运行,确保其不会被其他应用的 Activity 干扰,保证用户体验的一致性。
启动模式可通过在 AndroidManifest.xml 中为 Activity 配置 android:launchMode
属性设置,也可通过 Intent 的 FLAG_ACTIVITY_NEW_TASK
等标志位动态修改。不同模式的选择需结合业务场景,例如需要单例展示的页面用 singleTask,频繁跳转的普通页面用 standard,以平衡性能与用户体验。
Android 系统的核心架构基于四大组件,它们各司其职又相互协作,构成了应用的基本框架。
Activity 是与用户交互的可视化界面载体,负责展示 UI 和处理用户操作。其生命周期由系统管理,从创建到销毁经历 onCreate、onStart、onResume 等多个阶段,开发者需在不同阶段完成相应的资源管理。Activity 之间通过 Intent 进行跳转,可携带数据实现页面间通信,如在登录 Activity 中通过 Intent 传递用户信息到主界面 Activity。作为用户操作的主要入口,Activity 的设计直接影响应用的用户体验,需遵循轻量化原则,避免在其中处理耗时任务。
Service 是在后台执行长时间运行任务的组件,不提供用户界面,但可在后台处理数据下载、文件操作等任务。它有两种启动方式:“启动式” 通过 startService 启动,服务独立运行,即使启动它的组件销毁也会继续执行,需调用 stopService 停止;“绑定式” 通过 bindService 与客户端绑定,服务生命周期与绑定组件一致,可通过 ServiceConnection 实现客户端与服务的通信,如音乐播放器中通过 Service 管理播放逻辑,Activity 作为客户端绑定服务以控制播放。Service 运行在主线程中,因此耗时操作需在内部开启子线程,避免阻塞 UI。
BroadcastReceiver 用于接收系统或应用发出的广播消息,实现组件间的全局通信。广播分为 “普通广播” 和 “有序广播”,普通广播异步发送,所有接收器可同时接收;有序广播同步发送,接收器按优先级顺序接收。接收器可通过静态注册(在 Manifest 中声明)或动态注册(通过 registerReceiver 方法)监听特定广播,如监听网络状态变化、电池电量低等系统事件,或自定义广播实现应用内组件间的消息传递。广播接收器的 onReceive 方法运行在主线程,处理时间不宜超过 10 秒,否则会引发 ANR(应用无响应),复杂操作需启动 Service 处理。
ContentProvider 用于实现应用间的数据共享,封装了数据访问接口,其他应用可通过 ContentResolver 访问其提供的数据。它支持对数据库、文件、网络数据等的访问,如系统的 ContactsProvider 提供联系人数据访问。开发者自定义 ContentProvider 需继承该类并实现 query、insert、update、delete 等方法,同时通过 URI(如 content://com.example.provider/notes)唯一标识数据集合。ContentProvider 结合 ContentObserver 可实现数据变化的监听,确保多个应用间的数据一致性,是 Android 应用间数据交互的标准方式。
这四大组件通过 Intent、Binder 等机制相互协作,Activity 启动 Service、发送广播,BroadcastReceiver 响应事件后启动 Activity 或 Service,ContentProvider 为所有组件提供数据支持,共同构成了 Android 应用的完整生态。
在 Android 开发中,线程通信是解决 UI 主线程与子线程数据交互的关键,多种通信方式适用于不同场景,需根据需求选择合适的方案。
Handler 机制是最基础的线程通信方式,基于 Looper、MessageQueue 和 Handler 三者协作。Looper 负责循环读取 MessageQueue 中的消息,MessageQueue 存储消息队列,Handler 用于发送消息和处理消息。在主线程中,系统会自动创建 Looper 和 MessageQueue,开发者只需创建 Handler 并重写 handleMessage 方法处理消息。若在子线程中使用,需手动调用 Looper.prepare () 和 Looper.loop () 启动循环。例如,子线程中通过 handler.sendMessage (Message.obtain ()) 发送消息,主线程的 handleMessage 接收到消息后更新 UI,这种方式灵活高效,适用于需要频繁通信的场景,但需注意 Handler 可能导致的内存泄漏,可通过弱引用方式持有 Activity 引用。
AsyncTask 是对 Handler 的封装,将异步任务分为后台执行和 UI 更新两个阶段,简化了线程通信流程。它定义了 doInBackground(在后台线程执行耗时任务)、onPreExecute(执行前在主线程准备)、onPostExecute(完成后在主线程更新 UI)等方法。使用时只需继承 AsyncTask 并实现对应方法,如网络请求可在 doInBackground 中执行,结果通过 onPostExecute 返回给 UI。但 AsyncTask 在 Android 3.0 后默认串行执行,若需并行需调用 executeOnExecutor (AsyncTask.THREAD_POOL_EXECUTOR),且其生命周期与 Activity 不一致,Activity 销毁时若 AsyncTask 未完成,可能导致空指针异常,因此更适合短耗时任务。
HandlerThread 是自带 Looper 的线程类,继承自 Thread,创建时会自动启动 Looper。通过 getLooper () 获取其 Looper 后,可创建 Handler 与该线程通信,适用于需要长期运行的后台线程,如日志记录线程。例如,创建 HandlerThread 后,在其 Looper 上创建 Handler,后续通过该 Handler 发送消息,消息会在 HandlerThread 的循环中处理,避免了手动管理 Looper 的繁琐,同时保证了线程的独立性。
IntentService 是 Service 的子类,内部通过 HandlerThread 处理任务,确保耗时操作在后台执行。它会在 onHandleIntent 方法中处理传入的 Intent,处理完成后自动停止 Service,无需手动调用 stopSelf ()。例如,文件下载服务可继承 IntentService,每次下载请求通过 startService 发送 Intent,服务在后台线程中完成下载后自动销毁,简化了服务的管理,是处理异步任务的便捷方式。
MessageQueue 和 Looper 是 Handler 机制的底层基础,直接操作它们可实现更灵活的线程通信。例如,在子线程中创建 Looper,通过 Looper.myLooper () 获取后,创建 Handler 与主线程通信,这种方式适用于需要自定义消息循环策略的场景,但使用时需注意线程安全和资源释放。
此外,第三方库如 RxJava 通过响应式编程模型,利用 Observable 和 Observer 在不同线程间切换,实现异步任务和 UI 更新的解耦,适用于复杂的异步流程;EventBus 基于发布 - 订阅模式,通过注解方式注册事件接收器,简化了跨组件的线程通信,尤其适合组件间的广播式通信。
选择线程通信方式时,需考虑任务复杂度、生命周期管理和代码可读性。简单 UI 更新可使用 AsyncTask,频繁通信或复杂流程用 Handler,后台服务用 IntentService,而 RxJava 和 EventBus 则在大型项目中能有效提升代码的可维护性。
Handler 机制是 Android 消息处理的核心框架,其工作流程基于 “发布 - 订阅” 模式,通过 Message、MessageQueue 和 Looper 三者的协同实现线程间通信。当创建 Handler 时,它会自动关联当前线程的 Looper,Looper 负责从 MessageQueue 中循环取出消息并分发给 Handler 处理。具体来说,MessageQueue 是一个基于单链表实现的消息队列,用于存储 Handler 发送的 Message 对象;Looper 通过 loop () 方法不断从 MessageQueue 中获取消息,若队列为空则进入阻塞状态,直到有新消息插入时被唤醒。
Handler 可能导致内存泄漏,其核心原因在于非静态内部类对外部类的隐式引用。例如,在 Activity 中直接创建 Handler 实例时,Handler 作为内部类会持有 Activity 的强引用。当 Activity 销毁时,若 Handler 仍有未处理的消息或正在处理消息,这些消息会持有 Handler 的引用,进而导致 Activity 无法被垃圾回收,形成内存泄漏。具体泄漏流程如下:Activity -> Handler -> Message -> MessageQueue -> Looper -> Thread,由于 Thread 持有 Looper 的引用,最终导致 Activity 的生命周期被延长至 Thread 结束。
避免内存泄漏的常见做法是使用静态内部类搭配弱引用。静态内部类不持有外部类的引用,通过弱引用指向 Activity,即使 Activity 销毁,弱引用也能在垃圾回收时被正常回收。示例代码如下:
public class MainActivity extends AppCompatActivity {
private static class MyHandler extends Handler {
private WeakReference weakReference;
public MyHandler(MainActivity activity) {
weakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = weakReference.get();
if (activity != null) {
// 处理消息
}
}
}
private MyHandler handler = new MyHandler(this);
@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
Looper 的阻塞特性与 ANR(Application Not Responding)的触发机制存在本质区别。Looper 通过 loop () 方法实现消息循环,其内部通过 native 层的 pollOnce () 方法实现阻塞等待。当 MessageQueue 中没有待处理消息时,pollOnce () 会使线程进入休眠状态,此时线程并未真正阻塞,而是处于 “可唤醒” 的等待状态,系统资源消耗极低。一旦有新消息通过 enqueueMessage () 插入队列,内核会唤醒该线程,继续处理消息。
ANR 的触发条件是主线程(UI 线程)在规定时间内无法响应特定事件。例如,输入事件(如点击、滑动)需要在 5 秒内处理完成,前台 Service 的 onStartCommand () 需在 20 秒内执行完毕,后台 Service 则为 200 秒,广播接收器的 onReceive () 在前台场景下超过 10 秒、后台超过 60 秒未完成都会触发 ANR。系统通过 Watchdog 机制监控主线程的消息处理情况,当特定事件的处理时间超过阈值时,认为应用无响应,从而弹出 ANR 对话框。
Looper 的阻塞属于 “良性阻塞”,它不会影响主线程对消息的响应能力,因为阻塞状态是暂时的,且阻塞期间线程可以被快速唤醒。而 ANR 的本质是主线程在处理某个任务时被长时间阻塞,导致无法及时响应用户输入或系统事件。例如,在主线程中执行耗时的网络请求或大量计算时,会阻塞 Looper 的消息循环,使得输入事件无法被处理,超过 5 秒就会触发 ANR。
广播接收器(BroadcastReceiver)的生命周期非常短暂,onReceive () 方法的执行时间有限,若在其中直接执行耗时操作会导致 ANR。正确的做法是将耗时任务转移至其他组件处理,常见方案包括使用 IntentService、WorkManager 或启动 Service。
IntentService 是 Service 的子类,其内部通过 HandlerThread 处理消息,能自动处理异步任务并在任务完成后停止服务。使用时只需继承 IntentService 并实现 onHandleIntent () 方法,该方法在工作线程中执行,可安全处理耗时操作。示例如下:
public class DownloadIntentService extends IntentService {
public DownloadIntentService() {
super("DownloadIntentService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
// 执行下载等耗时操作
String url = intent.getStringExtra("url");
downloadFile(url);
}
}
在广播接收器中启动该服务:
Intent intent = new Intent(context, DownloadIntentService.class);
intent.putExtra("url", "http://example.com/file");
context.startService(intent);
对于 Android 5.0(API 21)以上版本,可使用 JobScheduler 实现周期性或条件触发的耗时任务。JobScheduler 基于系统调度,能根据网络状态、电量等条件优化任务执行时机,减少资源消耗。而 Android 8.0(API 26)推出的 WorkManager 则是更高级的封装,它结合了 JobScheduler、AlarmManager 等机制,支持任务调度、重试策略和周期性执行,且兼容低版本系统。
需要注意的是,广播接收器中获取 Context 时应使用 Application Context,避免因持有 Activity Context 导致内存泄漏。此外,对于有序广播,若在 onReceive () 中启动 Service,需注意在 Android 8.0 后,前台 Service 的启动受到限制,需明确声明通知渠道;对于后台服务,Android 10(API 29)后进一步限制了后台服务的使用,此时更推荐使用 WorkManager。
ANR 的触发源于主线程(UI 线程)在规定时间内无法完成特定任务,其核心条件包括以下几类场景,不同场景的超时阈值不同:
当用户进行点击、滑动等输入操作时,系统会向应用发送输入事件,应用需在 5 秒内完成事件处理。若主线程因耗时操作阻塞,无法在 5 秒内响应输入事件,就会触发 ANR。例如,在主线程中执行复杂的数据库查询或文件操作,导致输入事件堆积无法处理。
当应用通过 ContentProvider 提供数据时,query ()、insert ()、update ()、delete () 等方法需在 20 秒内完成。若这些方法中存在耗时操作,如复杂的游标查询或大量数据写入,会导致 ContentProvider 超时。
系统通过 Watchdog 机制检测 ANR,该机制由系统守护进程监控,定期检查主线程的消息循环是否卡顿。当特定事件的处理时间超过阈值时,系统会收集相关堆栈信息,生成 ANR 日志(存储在 /data/anr/traces.txt),并向用户显示 ANR 对话框。此外,Binder 调用超时也可能触发 ANR,例如应用向系统服务发送请求后,超过特定时间未收到响应,也会被判定为无响应。
Android 提供多种布局容器,适用于不同的界面结构需求:
优化布局性能的核心是减少视图层级、避免冗余渲染:
:合并布局层级,消除多余父容器。
:延迟加载的轻量级视图,初始时不渲染。
:引入外部布局,可重复使用布局模板。
:辅助线,用于定义约束参考位置。
:链式布局,控制一组视图的排列方式。
:屏障,根据一组视图的边界创建约束参考。布局优化需结合实际场景,例如在列表项布局中,应优先使用 LinearLayout 或 ConstraintLayout,避免 RelativeLayout 的复杂约束;对于需要动态加载的布局模块,合理使用 ViewStub 减少初始渲染耗时,从而提升应用启动速度和界面响应性能。
在 Android 开发中,多线程冲突通常源于多个线程同时访问共享资源,导致数据不一致或程序异常。解决这类问题需要结合 Java 并发编程机制与 Android 平台特性,常见的解决方案如下:
synchronized
是 Java 内置的同步机制,可作用于方法或代码块,确保同一时刻仅有一个线程执行被修饰的内容。其原理是通过对象监视器(Monitor)实现锁的获取与释放,例如:
private final Object lock = new Object();
void updateData() {
synchronized (lock) {
// 访问共享资源的代码
}
}
特点:自动加锁和释放锁,使用简单,但可能存在锁竞争导致的性能开销,且无法中断锁等待。
java.util.concurrent.locks.Lock
是一个接口,提供更灵活的锁控制,如可中断锁、公平锁、尝试获取锁等。ReentrantLock
是其常用实现,支持重入特性(线程可多次获取同一把锁):
private final ReentrantLock lock = new ReentrantLock();
void process() {
lock.lock();
try {
// 操作共享资源
} finally {
lock.unlock(); // 确保锁释放
}
}
优势:可通过 tryLock()
避免死锁,通过 lockInterruptibly()
响应中断,适合复杂场景下的精细控制。
java.util.concurrent.atomic
包中的原子类(如 AtomicInteger
、AtomicReference
)利用硬件级别的原子操作(CAS,Compare-And-Swap)实现线程安全,无需加锁即可保证操作的原子性。例如:
private AtomicInteger count = new AtomicInteger(0);
void increment() {
count.getAndIncrement(); // 原子性自增
}
原理:CAS 操作包含三个参数(内存值、预期值、新值),仅当内存值等于预期值时才更新为新值,避免了锁的开销,适合高频轻量级操作。
Java 并发包提供了线程安全的集合类,如 ConcurrentHashMap
、CopyOnWriteArrayList
,内部通过锁分段、写时复制等机制实现并发安全。例如:
ConcurrentHashMap
在 JDK 1.8 中通过 CAS 和 synchronized 结合,将锁粒度细化到链表或红黑树的头节点;CopyOnWriteArrayList
每次修改时复制底层数组,读操作无锁,适合读多写少场景。Android 中的 Handler
、Looper
、MessageQueue
机制虽主要用于线程通信,但也可间接解决冲突。例如,通过 Handler
将操作发送到主线程或特定工作线程,避免多线程同时访问 UI 组件:
private Handler mainHandler = new Handler(Looper.getMainLooper());
void updateUIOnMainThread() {
mainHandler.post(() -> {
// 更新 UI 组件
});
}
private Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
void accessResource() throws InterruptedException {
semaphore.acquire();
try {
// 访问资源
} finally {
semaphore.release();
}
}
ReentrantLock
;Handler
切换线程。在 Android 开发中,图片加载与缓存是性能优化的关键环节,合理的实现可避免内存溢出、减少网络请求消耗。其核心思想是构建 “三级缓存” 体系,并结合图片压缩、按需加载等策略。
1. 内存缓存(Memory Cache)
Runtime.getRuntime().maxMemory() / 8
)控制缓存大小: int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache memoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024; // 计算缓存项大小(KB)
}
};
2. 磁盘缓存(Disk Cache)
File cacheDir = new File(context.getCacheDir(), "image_cache");
DiskLruCache diskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024); // 50MB 缓存空间
3. 网络加载(Network Load)
Cache-Control
或 Expires
,控制客户端缓存时长。1. 主流库对比
库 | 特点 | 适用场景 |
---|---|---|
Glide | 支持动图、WebP、内存缓存优化(Bitmap 复用),Android 兼容性强 | 通用场景,推荐首选 |
Picasso | 简洁易用,支持请求队列和优先级,适合小项目 | 轻量级图片加载 |
Fresco | 独立图片内存池,适合加载超大图片,性能开销较大 | 新闻、电商等大图场景 |
2. Glide 核心机制
BitmapRegionDecoder
按需解码大图局部,减少内存占用;ActivityLifecycleCallbacks
监听页面生命周期,自动取消不可见页面的加载请求。inSampleSize
),避免加载全尺寸图片: BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap scaledBitmap = BitmapFactory.decodeFile(path, options);
onDestroy
清理缓存。RecyclerView.OnScrollListener
控制;RoundedBitmapDrawable
或自定义 Transformation
缓存处理结果;在 Java 编程中,Collection
和 Collections
是两个极易混淆的概念,前者是集合框架的核心接口,后者是操作集合的工具类,二者在功能、用途和实现上存在本质差异。
1. 接口定义与层级
java.util.Collection
是 Java 集合框架的根接口之一,定义了集合(如列表、集合、队列)的通用操作方法,例如:
add(E e)
:添加元素;remove(Object o)
:删除元素;contains(Object o)
:判断是否包含元素;size()
:获取元素数量;iterator()
:返回迭代器。2. 子接口与实现类
ArrayList
、LinkedList
、Vector
;HashSet
、TreeSet
、LinkedHashSet
;LinkedList
、PriorityQueue
。3. 核心特性
ArrayList
基于数组实现,LinkedList
基于链表实现。java.util.Collections
是一个包含静态方法的工具类,用于操作集合或创建特殊集合,其方法大致可分为以下几类:
sort(List list)
对列表进行自然排序,sort(List list, Comparator super T> c)
自定义排序规则: List names = new ArrayList<>();
names.add("Bob");
names.add("Alice");
Collections.sort(names); // 按字母顺序排序
binarySearch(List extends Comparable super T>> list, T key)
二分查找元素位置,replaceAll(List list, T oldVal, T newVal)
替换所有指定元素。通过 synchronizedList
、synchronizedSet
等方法将非线程安全的集合转为线程安全版本:
List safeList = Collections.synchronizedList(new ArrayList<>());
// 访问时需手动同步
synchronized (safeList) {
for (String item : safeList) {
// 操作元素
}
}
unmodifiableList
、unmodifiableSet
等方法创建不可修改的集合,防止外部篡改: List original = new ArrayList<>();
List immutable = Collections.unmodifiableList(original);
// immutable.add("element") 会抛出 UnsupportedOperationException
emptyList
、emptySet
返回类型安全的空集合,避免 null
指针异常。reverse(List> list)
:反转列表元素顺序;shuffle(List> list)
:随机打乱列表元素;max/ min(Collection extends T> coll)
:获取集合中的最大 / 最小值。维度 | Collection | Collections |
---|---|---|
本质 | 接口(interface) | 工具类(class,且构造方法私有) |
功能 | 定义集合的基本操作规范 | 提供操作集合的静态工具方法 |
是否可实例化 | 不可(接口无实现) | 不可(构造方法私有,仅含静态方法) |
典型用法 | 作为参数类型(如 void process(Collection ) |
对集合进行排序、同步、不可变包装等 |
Collection
接口作为参数或返回值,体现面向接口编程: // 良好实践:方法参数使用接口而非实现类
void processData(Collection data) {
// 处理集合数据
}
Collections
工具类,例如对列表排序、创建线程安全集合: // 对学生列表按成绩排序
Collections.sort(students, (s1, s2) -> s2.getScore() - s1.getScore());
HashMap 是 Java 中常用的键值对存储容器,其底层实现结合了数组、链表和红黑树(JDK 1.8 及之后),通过哈希算法实现高效的查找与插入。理解其底层结构对性能优化和问题排查至关重要。
1. JDK 1.7:数组 + 链表
Entry
数组,每个元素是一个链表的头节点,链表存储哈希冲突的键值对;2. JDK 1.8:数组 + 链表 + 红黑树
Node
数组,当链表长度超过阈值(默认 8)且数组容量超过 64 时,链表转换为红黑树,查找时间复杂度优化为 O (log n);transient Node[] table; // 存储数据的数组,又称“桶”(bucket)
transient int size; // 键值对数量
int threshold; // 扩容阈值,等于 capacity * loadFactor
final float loadFactor; // 负载因子,默认 0.75f,控制数组扩容时机
1. 哈希值计算(hash (key) 方法)
JDK 1.8 中,哈希值计算如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key.hashCode()
获取哈希码,再与右移 16 位的自身异或,使高位特征也参与低位计算,减少哈希冲突。2. 插入操作(put (K key, V value))
步骤如下:
i = (n - 1) & hash
(n 为数组长度,必须是 2 的幂,保证 (n-1)&hash
等价于 hash % n
,且运算更快);3. 查找操作(get (Object key))
当 HashMap 中的元素数量超过扩容阈值时,会触发扩容:
(n-1)&hash
结果仅取决于 hash 值新增的高位,可优化为 if ((e.hash & oldCap) == 0) 位置不变,否则位置为原下标 + oldCap
);ConcurrentHashMap
实现)。1. 优势
2. 不足
ConcurrentHashMap 是 Java 并发包中提供的线程安全哈希表,相比 HashMap,它通过更精细的锁策略和数据结构优化,在高并发场景下实现了高效的读写操作。其底层实现随 JDK 版本迭代发生了显著变化。
1. 核心结构
HashEntry
数组,结构类似 JDK 1.7 的 HashMap,通过链表处理哈希冲突。2. 关键属性
final Segment[] segments; // Segment 数组
transient volatile int size; // 元素总数
3. 并发控制
volatile
修饰的引用直接获取值),仅在获取迭代器等场景下需要;4. 优势与不足
Hashtable
的全表锁性能显著提升;1. 结构优化
Node
数组 + 链表 + 红黑树结构;synchronized
替代 Segment 锁,结合 CAS 操作实现高效并发控制。2. 核心属性
transient volatile Node[] table; // 存储数据的数组
private transient volatile int sizeCtl; // 控制数组初始化和扩容的状态值
3. 关键操作实现
(1)初始化(initTable ())
if ((tab = table) == null || tab.length == 0) {
int h = threadLocalRandomProbe.get();
int rs = 0;
// 通过 CAS 尝试设置 sizeCtl 为 -1(表示正在初始化)
if (U.compareAndSwapInt(this, SIZECTL, 0, -1)) {
try {
// 初始化数组
} finally {
sizeCtl = n; // n 为数组容量
}
}
}
(2)插入操作(put (K key, V value))
步骤如下:
synchronized
锁,判断是链表还是红黑树,执行插入操作;addCount()
方法更新元素数量,可能触发扩容。(3)扩容操作(transfer ())
ForwardingNode
标记已处理的桶,其他线程遇到该标记会协助扩容;(4)读操作(get (Object key))
volatile
修饰,保证可见性。维度 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全性 | 非线程安全 | 线程安全,支持高并发读写 |
锁策略 | 无锁 | JDK 1.7 分段锁,JDK 1.8 细粒度 synchronized + CAS |
null key/value | 支持 key 和 value 为 null | 不支持 key 为 null(会抛出 NullPointerException),value 可为 null |
扩容机制 | 单线程扩容 | 支持并发扩容,通过 ForwardingNode 协作迁移 |
迭代器 | fail-fast(遍历时修改会抛异常) | 弱一致性迭代器,遍历时允许其他线程修改 |
volatile
和 CAS 保证数据可见性与原子性;HashMap 和 Hashtable 均为 Java 中存储键值对的集合类,但在设计理念、线程安全性和功能特性上存在显著差异,这些差异源于它们的历史背景和应用场景。
put
、get
)均被 synchronized
修饰,确保同一时刻仅有一个线程访问集合。但这种全表锁的方式在高并发场景下性能开销较大。key
和 value
为 null
,但 null key
只能有一个(因为哈希值固定为 0)。例如: HashMap map = new HashMap<>();
map.put(null, 1); // 合法
map.put("key", null); // 合法
key
或 value
为 null
,若尝试插入 null
,会抛出 NullPointerException
。这是因为其 put
方法在插入前会检查键值是否为 null
: Hashtable table = new Hashtable<>();
table.put(null, 1); // 抛出 NullPointerException
AbstractMap
,实现了 Map
、Cloneable
和 Serializable
接口,支持克隆和序列化。Dictionary
(Java 早期的集合接口),实现了 Map
、Cloneable
和 Serializable
接口,但 Dictionary
已被 Map
替代,属于遗留类。维度 | HashMap | Hashtable |
---|---|---|
初始容量 | 默认 16 | 默认 11 |
负载因子 | 0.75(可自定义) | 0.75(可自定义) |
扩容策略 | 容量达到 threshold (容量 × 负载因子)时,新容量为原容量的 2 倍 |
容量达到 threshold 时,新容量为原容量的 2 倍 + 1 |
扩容效率 | 采用位运算 (n - 1) & hash 计算下标,效率更高 |
直接使用 hash % size ,性能略低 |
ConcurrentHashMap
。ConcurrentModificationException
;ArrayList 和 LinkedList 均为 Java 中 List
接口的实现类,但底层数据结构不同,导致它们在性能特性和适用场景上存在明显差异。
操作类型 | ArrayList | LinkedList |
---|---|---|
随机访问 | 时间复杂度 O (1),直接通过索引获取元素 | 时间复杂度 O (n),需遍历链表查找 |
头部插入 / 删除 | 时间复杂度 O (n),需移动后续元素 | 时间复杂度 O (1),仅修改指针 |
中间插入 / 删除 | 时间复杂度 O (n),需移动相邻元素 | 时间复杂度 O (1)(已知节点位置) |
尾部插入 | 平均时间复杂度 O (1)(无需扩容时) | 时间复杂度 O (1) |
Arrays.copyOf
实现),例如初始容量 10,扩容后为 15。Collections.synchronizedList
包装)。addFirst
、addLast
、getFirst
、getLast
等方法,适合作为队列或栈使用;ensureCapacity
预分配容量,减少扩容次数。ensureCapacity
优化性能;Queue
接口)或双端队列(Deque
接口)使用时(LinkedList 实现了这两个接口)。new ArrayList<>(initialCapacity)
预分配容量,避免多次扩容;Iterator
)而非普通 for 循环,减少指针跳转次数;CopyOnWriteArrayList
替代同步包装的 ArrayList/LinkedList,提升读性能。Java 并发包(java.util.concurrent
)提供了多种线程安全的队列实现,根据功能特性可分为阻塞队列和非阻塞队列,适用于不同的并发场景。
阻塞队列支持线程阻塞等待,当队列为空时获取元素的线程会阻塞,当队列满时插入元素的线程会阻塞,常用于生产者 - 消费者模式。
1. ArrayBlockingQueue
ReentrantLock
实现线程安全,插入和获取操作均为阻塞操作。2. LinkedBlockingQueue
3. PriorityBlockingQueue
Comparable
或自定义 Comparator
),获取操作返回优先级最高的元素。4. DelayQueue
Delayed
接口,只有到期后才能被获取。5. SynchronousQueue
ExecutorService
的 newCachedThreadPool
底层使用该队列。非阻塞队列使用 CAS(Compare-And-Swap)操作实现线程安全,不会阻塞线程,但可能需要重试操作。
1. ConcurrentLinkedQueue
2. ConcurrentLinkedDeque
1. LinkedTransferQueue
BlockingQueue
,支持 transfer
方法(插入元素并等待消费者获取),性能优于 SynchronousQueue
。2. LinkedBlockingDeque
ArrayBlockingQueue
(有界)或 LinkedBlockingQueue
(无界),根据任务量选择队列类型;ConcurrentLinkedQueue
,避免锁竞争;PriorityBlockingQueue
;DelayQueue
;SynchronousQueue
或 LinkedTransferQueue
。StringBuffer 和 StringBuilder 均为 Java 中用于处理可变字符串的类,它们共享相同的底层实现,但在线程安全性和性能表现上存在显著差异,适用于不同的编程场景。
append
、insert
)均被 synchronized
修饰,确保多线程环境下操作的原子性。例如: StringBuffer buffer = new StringBuffer();
buffer.append("hello"); // 同步方法,线程安全
StringBuilder builder = new StringBuilder();
builder.append("world"); // 非同步方法,单线程安全
AbstractStringBuilder
,使用字符数组 char[] value
存储字符串,默认容量为 16,超过容量时通过 Arrays.copyOf
扩容(新容量为原容量的 2 倍 + 2)。维度 | String | StringBuffer | StringBuilder |
---|---|---|---|
不可变性 | 不可变,每次操作生成新对象 | 可变,直接修改字符数组 | 可变,直接修改字符数组 |
线程安全 | 无 | 安全 | 不安全 |
性能 | 低(频繁生成新对象) | 中(同步开销) | 高(无同步开销) |
适用场景 | 常量字符串、不可变场景 | 多线程可变字符串 | 单线程可变字符串 |
+
操作符(会生成多个 String
对象),应使用 StringBuilder
: // 低效做法
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 每次拼接生成新 String 对象
}
// 高效做法
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 直接修改字符数组,避免对象创建
}
StringBuilder(int capacity)
预分配容量,减少扩容次数;StringBuilder
,可通过 synchronized
手动加锁: StringBuilder builder = new StringBuilder();
synchronized (builder) {
builder.append("data");
}
单例模式是一种创建型设计模式,确保类在全局范围内仅有一个实例,并提供统一的访问入口。Java 中实现单例模式需解决线程安全、延迟初始化、序列化安全等问题,常见实现方式如下。
核心思想:类加载时直接初始化实例,确保线程安全。
public class HungrySingleton {
// 静态常量直接初始化实例
private static final HungrySingleton instance = new HungrySingleton();
// 私有构造函数,防止外部实例化
private HungrySingleton() {}
// 公共访问方法
public static HungrySingleton getInstance() {
return instance;
}
}
特点:
核心思想:延迟初始化,首次使用时创建实例。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
// 未同步的获取方法,线程不安全
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 可能被多个线程同时创建
}
return instance;
}
}
风险:多线程环境下可能创建多个实例,需配合同步机制改进。
核心思想:通过 synchronized
保证线程安全。
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
// 同步方法,线程安全但性能开销大
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
缺点:每次获取实例都需加锁,并发场景下性能较低。
核心思想:通过双重 null 检查和 volatile
关键字优化同步性能。
public class DoubleCheckSingleton {
// 必须使用 volatile 防止指令重排序
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一次检查,避免无意义加锁
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // 第二次检查,确保唯一实例
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
关键细节:
volatile
禁止指令重排序,确保 instance 初始化的可见性;核心思想:利用类加载机制实现延迟初始化和线程安全。
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类在外部类被访问时才加载
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理:
核心思想:利用枚举的特性实现单例,简洁且安全。
public enum EnumSingleton {
INSTANCE;
// 可添加自定义方法
public void doSomething() {
// 业务逻辑
}
}
优势:
实现方式 | 线程安全 | 延迟初始化 | 抗反射攻击 | 抗序列化攻击 | 推荐场景 |
---|---|---|---|---|---|
饿汉式 | 是 | 否 | 是 | 是 | 实例占用资源少,需提前初始化 |
懒汉式(同步) | 是 | 是 | 是 | 否 | 简单场景,性能要求不高 |
DCL | 是 | 是 | 是 | 否 | 高性能并发场景 |
静态内部类 | 是 | 是 | 是 | 否 | 通用场景,推荐使用 |
枚举 | 是 | 否 | 是 | 是 | 需支持序列化,推荐使用 |
readResolve()
方法防止反序列化时创建新实例,枚举单例自动解决此问题。双检锁(Double-Checked Locking)是单例模式的一种实现技巧,其核心目的是在保证线程安全的同时提升性能。具体实现中,会通过两次判断实例是否为空(第一次不锁,第二次加锁后再判断),避免频繁加锁带来的开销。
双检锁的典型代码结构如下:
public class Singleton {
private volatile static Singleton instance; // 声明volatile防止指令重排序
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:未加锁时判断,减少锁竞争
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:加锁后再次判断,避免多线程创建多个实例
instance = new Singleton(); // 这里可能发生指令重排序
}
}
}
return instance;
}
}
两次检查的意义:
此外,volatile
关键字在此处必不可少。因为instance = new Singleton()
并非原子操作,可能被 JVM 优化为以下步骤:
volatile
,步骤 2 和 3 可能发生重排序(先赋值再初始化)。此时,若一个线程获取到未完全初始化的 instance,访问时可能引发异常。volatile
禁止指令重排序,确保实例初始化完成后才被其他线程访问。Java 多线程是指在一个程序中同时运行多个执行流,每个线程独立执行任务,从而提升程序的并发处理能力。理解多线程需从以下几个维度切入:
Java 线程通过Thread
类管理,其状态包括:
Thread
对象但未调用start()
方法。start()
后进入就绪队列,等待 CPU 调度。Thread
类:封装线程操作,如start()
、join()
、interrupt()
;Runnable
接口:定义线程执行逻辑,推荐使用(避免继承限制);ExecutorService
:线程池框架,管理线程生命周期,避免频繁创建线程;Concurrent包
:提供线程安全的集合(如ConcurrentHashMap
)、同步工具(如CountDownLatch
)。Java 中实现线程主要有以下四种方式,每种方式的适用场景和特性各不相同:
通过子类化Thread
类,重写run()
方法定义线程逻辑:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
// 使用方式
MyThread thread = new MyThread();
thread.start();
特点:
定义实现Runnable
接口的类,将线程逻辑放入run()
方法:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
// 使用方式
Thread thread = new Thread(new MyRunnable());
thread.start();
特点:
Runnable
可共享同一资源(如计数器),适合资源共享场景。Callable
接口支持泛型返回值和异常抛出,需配合FutureTask
使用:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable {
@Override
public String call() throws Exception {
// 可抛出异常,返回计算结果
return "Callable result";
}
}
// 使用方式
Callable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 获取结果
try {
String result = futureTask.get(); // 阻塞等待结果
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
特点:
get()
方法会阻塞线程,需注意性能影响。通过ExecutorService
框架管理线程,避免手动创建线程的开销:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建线程池(固定大小为5)
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交Runnable任务
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("Task from thread pool");
}
});
// 提交Callable任务
executor.submit(new Callable() {
@Override
public Void call() throws Exception {
// 执行任务
return null;
}
});
// 关闭线程池
executor.shutdown();
特点:
FixedThreadPool
、CachedThreadPool
);线程通信是多线程编程的核心问题,用于协调多个线程的执行顺序或共享数据。Java 和 Android 中提供了多种通信机制,适用于不同场景:
通过共享变量传递信息,配合synchronized
、Lock
等同步工具保证线程安全:
private int count = 0;
private final Lock lock = new ReentrantLock();
// 线程A修改数据
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
// 线程B读取数据
lock.lock();
try {
int value = count;
} finally {
lock.unlock();
}
适用场景:简单数据交换,需注意原子性和可见性(如使用volatile
修饰变量)。
通过wait()
让线程等待,notify()
/notifyAll()
唤醒等待线程,需在synchronized
块中使用:
private final Object lock = new Object();
// 线程A等待数据
synchronized (lock) {
while (dataNotReady) {
lock.wait(); // 释放锁并进入等待队列
}
processData();
}
// 线程B准备好数据后唤醒
synchronized (lock) {
prepareData();
dataNotReady = false;
lock.notify(); // 唤醒一个等待线程
}
特点:底层基于 JVM 监视器(Monitor)实现,是最基础的线程通信方式,但需手动处理等待条件和唤醒逻辑。
允许一个线程等待其他线程完成任务后再执行,通过计数器控制:
import java.util.concurrent.CountDownLatch;
CountDownLatch latch = new CountDownLatch(3); // 计数器初始化为3
// 线程1-3完成任务后计数减1
new Thread(() -> {
doTask();
latch.countDown();
}).start();
// 主线程等待计数器归零
latch.await(); // 阻塞直到计数器为0
System.out.println("All tasks completed");
适用场景:一次性等待多个线程完成,如初始化阶段等待资源加载。
多个线程互相等待,直到所有线程到达屏障点后再一起执行:
import java.util.concurrent.CyclicBarrier;
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads arrived");
});
// 三个线程各自执行到barrier.await()时等待
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
doTask();
barrier.await(); // 等待其他线程
continueTask();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
特点:计数器可重置(Cyclic),适合重复使用的场景,如多阶段任务调度。
控制同时访问资源的线程数量,常用于限流:
import java.util.concurrent.Semaphore;
Semaphore semaphore = new Semaphore(2); // 允许2个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可,无许可时阻塞
accessResource();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
适用场景:控制数据库连接数、文件句柄等有限资源的访问。
通过队列实现线程间数据传递,支持阻塞操作(队满时插入阻塞,队空时取出阻塞):
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
BlockingQueue queue = new ArrayBlockingQueue<>(10);
// 生产者线程
new Thread(() -> {
while (true) {
String data = produceData();
try {
queue.put(data); // 队满时阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 消费者线程
new Thread(() -> {
while (true) {
try {
String data = queue.take(); // 队空时阻塞
consumeData(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
特点:天然支持生产者 - 消费者模式,代码结构清晰,是推荐的通信方式之一。
在 Android 中,通过Handler
实现主线程(UI 线程)与子线程的通信:
// 在主线程创建Handler
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
updateUI((String) msg.obj); // 更新UI
}
}
};
// 子线程发送消息
new Thread(() -> {
String data = fetchDataFromNetwork();
Message message = Message.obtain();
message.what = 1;
message.obj = data;
handler.sendMessage(message); // 发送消息到主线程Looper
}).start();
适用场景:Android 中跨线程更新 UI,底层依赖Looper
和MessageQueue
机制。
守护线程(Daemon Thread)是 Java 中一种特殊的线程,其生命周期依赖于用户线程(Non-Daemon Thread),主要用于执行后台支持性任务,如垃圾回收、日志监控等。
特性 | 守护线程 | 用户线程 |
---|---|---|
生命周期 | 随最后一个用户线程结束而终止 | 独立运行,需主动结束 |
用途 | 后台辅助任务(如 GC) | 执行具体业务逻辑 |
启动方式 | 需在start() 前设置为守护线程 |
直接启动 |
终止条件 | 无用户线程时自动终止 | 任务完成或主动中断 |
通过Thread.setDaemon(true)
方法将线程标记为守护线程,且必须在start()
之前调用:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
try {
System.out.println("Daemon thread running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start(); // 启动线程
try {
Thread.sleep(3000); // 主线程运行3秒后结束
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程结束后,守护线程自动终止
}
}
注意事项:
start()
后调用setDaemon(true)
,会抛出IllegalThreadStateException
;finally
块或shutdown
逻辑。死锁是指在多线程或多进程环境中,两个或多个进程因争夺资源而造成的一种互相等待的状态,若无外力干涉则这些进程永远无法推进。死锁的发生必须同时满足四个必要条件:互斥条件(资源不能被共享,一次只能被一个进程使用)、请求与保持条件(进程已获得资源,同时又对其他资源发出请求)、不剥夺条件(进程已获得的资源在未使用完前不能被剥夺)、循环等待条件(存在一种进程资源的循环等待链)。
避免死锁的核心在于破坏这四个必要条件中的任意一个,常见方法包括:
实际开发中,需通过合理的资源管理策略和代码设计来预防死锁,例如数据库事务中按固定顺序加锁、避免嵌套锁等。
volatile 的作用与原理:
volatile 是 Java 中的轻量级同步机制,用于保证变量的可见性和有序性,但不保证原子性。当变量被 volatile 修饰时,线程对该变量的读取会直接从主内存获取,写入时会立即刷新到主内存,避免线程本地缓存导致的可见性问题。此外,volatile 会禁止指令重排序,确保代码执行顺序与书写顺序一致(仅针对 volatile 变量的操作)。例如,在双重检查锁定单例模式中,volatile 用于防止指令重排序导致的初始化异常。
synchronized 的作用与原理:
synchronized 是 Java 中的内置锁,可修饰方法、代码块,用于保证代码的原子性、可见性和有序性。其底层通过 JVM 的 Monitor 机制实现,当线程获取锁时,会标记对象头的 Monitor 记录,执行完同步代码块后释放锁,其他线程才能获取。synchronized 是重量级锁,涉及线程状态切换和操作系统内核态与用户态的转换,性能开销较大,但在 JDK1.6 后通过偏向锁、轻量级锁等优化,性能有所提升。
两者的核心区别:
维度 | volatile | synchronized |
---|---|---|
原子性 | 不保证,仅保证可见性和有序性 | 保证代码块或方法的原子性 |
锁范围 | 修饰变量 | 修饰方法或代码块 |
性能开销 | 开销小,无线程阻塞 | 开销大,可能导致线程阻塞 |
底层实现 | JVM 内存模型规范 | 依赖 Monitor 机制(对象头标记) |
适用场景 | 多线程读多线程写的变量可见性 | 多线程竞争资源的同步操作 |
Java 的垃圾回收(GC)算法用于识别和回收不再使用的对象,释放内存空间。常见的 GC 算法包括:
当新生代 Eden 区满时触发 Minor GC,用复制算法回收;当老年代空间不足时触发 Major GC(Full GC),用标记 - 整理算法回收。分代收集通过对不同生命周期的对象采用针对性算法,平衡了回收效率和内存利用率。
Java 中有四种引用类型,按强度从高到低依次为:
Object obj = new Object();
,只要强引用存在,对象就不会被 GC 回收。SoftReference
类实现,当系统内存不足时,才会回收软引用指向的对象。SoftReference softRef = new SoftReference<>(bitmap);
Bitmap bitmap = softRef.get(); // 需判空
WeakReference
类实现,弱引用的对象会在下次 GC 时被回收,无论内存是否充足。ReferenceQueue
PhantomReference
类实现,最弱的引用类型,无法通过虚引用获取对象实例,仅用于对象被回收时接收通知。引用类型 | GC 时是否回收 | 对象可达性 | 典型应用场景 |
---|---|---|---|
强引用 | 否 | 始终可达 | 普通对象引用 |
软引用 | 内存不足时回收 | 内存充足时可达 | 缓存(如图片、数据缓存) |
弱引用 | 下次 GC 时回收 | 仅弱引用存在时 | 防止内存泄漏、弱键 HashMap |
虚引用 | 立即回收 | 不可达 | 对象回收通知、资源清理 |
TCP(传输控制协议)和 UDP(用户数据报协议)是 TCP/IP 协议栈中两种核心的传输层协议,在设计理念和应用场景上有显著差异:
维度 | TCP | UDP |
---|---|---|
连接性 | 面向连接(三次握手、四次挥手) | 无连接 |
可靠性 | 可靠传输(保证顺序和完整性) | 不可靠传输(不保证交付) |
传输单位 | 字节流(Byte Stream) | 数据报(Datagram) |
头部开销 | 20 字节(固定)+ 可选字段 | 8 字节(固定) |
流量控制 | 支持(滑动窗口) | 不支持 |
拥塞控制 | 支持(慢启动、拥塞避免) | 不支持 |
实时性 | 较低(连接建立和重传开销) | 较高(无连接和重传机制) |
应用场景 | 对可靠性要求高的场景 | 对实时性要求高的场景 |
OSI(Open System Interconnection)五层模型是计算机网络通信的理论框架,将网络通信的过程划分为不同层次,每层负责特定功能,各层之间相互独立又协同工作。具体如下:
应用层是模型的最高层,直接为用户应用程序提供服务,处理用户接口、数据格式转换等。例如 HTTP、FTP、SMTP 协议,分别用于网页浏览、文件传输和电子邮件。应用层关注的是应用程序的逻辑,如浏览器如何解析 HTML 文档,邮件客户端如何发送邮件。
传输层负责端到端的数据传输,确保数据可靠传输或高效传输。主要协议包括 TCP 和 UDP。TCP(传输控制协议)提供面向连接的可靠传输,通过三次握手建立连接,使用确认机制和重传策略确保数据无误;UDP(用户数据报协议)是无连接的不可靠传输,适合对实时性要求高的场景,如视频流、直播。传输层通过端口号区分不同应用程序,如 HTTP 默认端口 80,HTTPS 默认端口 443。
网络层负责网络间的路由选择和 IP 地址寻址,将数据从源主机路由到目标主机。主要协议是 IP 协议,规定了数据包的格式和路由规则。此外,ICMP(互联网控制报文协议)用于网络诊断,如 ping 命令;ARP(地址解析协议)用于将 IP 地址映射为 MAC 地址。网络层解决的是 “在哪里” 的问题,确保数据能从一个网络传输到另一个网络。
数据链路层在物理层之上,负责相邻节点间的数据传输,将比特流组织成帧,并处理错误检测和流量控制。常见协议有 PPP(点对点协议)、Ethernet(以太网协议)。它通过 MAC 地址标识设备,实现局域网内的数据传输,例如交换机通过学习 MAC 地址来转发数据帧。数据链路层还会进行帧同步,确保接收方正确解析数据。
物理层是模型的最底层,负责二进制比特流的传输,定义物理设备的电气特性、接口标准等。例如网线的接口类型、信号的电压范围、光纤的传输方式。物理层不关心数据的内容,只负责将数据转化为电信号或光信号在介质中传输,是网络通信的物理基础。
HTTP(Hypertext Transfer Protocol)报文是客户端与服务器之间通信的载体,分为请求报文和响应报文,两者结构相似但内容不同。
GET /index.html HTTP/1.1
,其中 GET 是方法,/index.html
是路径,HTTP/1.1 是版本。User-Agent
:标识客户端类型,如浏览器信息。Accept
:客户端接受的响应数据类型,如text/html
。Cookie
:携带服务器之前设置的 Cookie 信息。Content-Type
:请求体的数据类型,如application/json
。HTTP/1.1 200 OK
。
Content-Type
:响应体的数据类型。Content-Length
:响应体的字节长度。Set-Cookie
:服务器设置 Cookie,让客户端后续请求携带。Cache-Control
:缓存控制策略。类型 | 组成部分 | 示例内容 |
---|---|---|
请求报文 | 请求行 | GET /api/data HTTP/1.1 |
请求头部 | User-Agent: Mozilla/5.0 |
|
空行 | \r\n |
|
请求体 | {"key": "value"} (仅 POST 等方法存在) |
|
响应报文 | 状态行 | HTTP/1.1 200 OK |
响应头部 | Content-Type: application/json |
|
空行 | \r\n |
|
响应体 | {"code": 200, "message": "success"} |
HTTP 报文通过 ASCII 码编码,头部字段不区分大小写,键值对用冒号分隔。了解报文结构有助于分析网络请求问题,如抓包工具(Fiddler、Charles)中显示的报文内容即为此格式。
HTTP 协议定义了多种请求方法,其中 GET 和 POST 是最常用的两种获取数据的方式,两者在设计理念、使用场景上有明显区别。
GET 请求的核心目的是 “获取资源”,将请求参数附加在 URL 中,格式为URL?参数名=参数值&参数名=参数值
。例如:https://example.com/api/user?userId=123&type=admin
。
POST 请求将参数放在请求体中,URL 仅表示资源路径,不包含参数。例如:URL 为https://example.com/api/user
,请求体为{"userId": 123, "type": "admin"}
。
维度 | GET 方法 | POST 方法 |
---|---|---|
参数位置 | URL 查询字符串 | 请求体 |
安全性 | 低(参数可见) | 高(参数在请求体) |
幂等性 | 是 | 否 |
长度限制 | 有(受 URL 限制) | 无(受服务器限制) |
缓存支持 | 支持 | 通常不支持 |
数据类型 | 文本(适合简单参数) | 任意类型(如 JSON、文件) |
此外,HTTP 还有 PUT(更新资源)、DELETE(删除资源)、HEAD(获取头部)等方法,但 GET 和 POST 最常用。在 Android 开发中,通常使用 OkHttp 或 Retrofit 库发送请求,例如:
// GET请求示例(Retrofit)
@GET("api/user")
Call getUser(@Query("userId") int userId);
// POST请求示例(Retrofit)
@POST("api/user")
Call saveUser(@Body User user);
选择 GET 或 POST 需根据业务场景:若仅需获取数据且参数简单,用 GET;若需提交数据或参数复杂,用 POST。
在 TCP 通信中,粘包(多个数据包合并)和分包(一个数据包被拆分)是常见问题,本质原因是 TCP 作为流协议,没有明确的消息边界。
00000101
(二进制),接收方先读 4 字节得长度 5,再读 5 字节消息。\r\n
)作为消息边界。HTTP 协议即采用此方法,请求头和请求体用空行(\r\n
)分隔。
\n
作为分隔符。{"length": 1024, "data": "..."}
,接收方先解析 length 字段再读数据。TCP_NODELAY
参数禁用该算法,强制立即发送数据,适用于实时性要求高的场景(如游戏)。在 Socket 通信中,可通过自定义协议处理粘包分包:
// 读取消息(假设头部4字节表示长度)
public byte[] readMessage(InputStream inputStream) throws IOException {
byte[] lengthBytes = new byte[4];
inputStream.read(lengthBytes); // 先读长度
int length = bytesToInt(lengthBytes);
byte[] message = new byte[length];
inputStream.read(message); // 再读完整消息
return message;
}
// 发送消息
public void sendMessage(OutputStream outputStream, byte[] data) throws IOException {
byte[] lengthBytes = intToBytes(data.length); // 转换长度为4字节
outputStream.write(lengthBytes);
outputStream.write(data);
outputStream.flush();
}
解决粘包分包问题的核心是在应用层建立消息边界,让接收方能够正确解析每个独立的消息,确保数据完整性和准确性。
下载进度条的显示涉及 UI 更新和耗时操作处理,而数据大小不确定时需特殊处理,确保用户体验。
Content-Length
字段获取文件总大小(单位:字节)。(已下载/总大小)*100%
得到进度百分比。AsyncTask
、Service
或协程
)。Handler
或回调机制将进度传递给主线程更新 UI。public class DownloadTask extends AsyncTask {
private ProgressBar progressBar;
private TextView progressText;
private Context context;
public DownloadTask(Context context, ProgressBar progressBar, TextView progressText) {
this.context = context;
this.progressBar = progressBar;
this.progressText = progressText;
}
@Override
protected String doInBackground(String... urls) {
try {
URL url = new URL(urls[0]);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
int totalSize = connection.getContentLength();
progressBar.setMax(totalSize);
InputStream inputStream = connection.getInputStream();
FileOutputStream outputStream = new FileOutputStream("downloaded_file");
byte[] buffer = new byte[1024];
int len;
int downloaded = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
downloaded += len;
// 计算进度并通知UI更新
int progress = (int) ((downloaded * 100) / totalSize);
publishProgress(progress);
}
outputStream.close();
inputStream.close();
return "下载完成";
}
} catch (Exception e) {
e.printStackTrace();
return "下载失败";
}
return "下载取消";
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
progressBar.setProgress(values[0]);
progressText.setText(values[0] + "%");
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
Toast.makeText(context, result, Toast.LENGTH_SHORT).show();
}
}
ProgressBar
:Android 原生组件,支持水平进度(style="?android:attr/progressBarStyleHorizontal"
)和圆形进度(默认)。View
或ProgressBar
自定义绘制逻辑。当服务器未提供Content-Length
(如流式响应、动态生成的数据),或下载内容为分段数据(如直播流),无法获取总大小时,需采用以下策略:
ProgressBar
设置为indeterminate
模式(圆形旋转或水平波浪动画),表示 “正在加载”,不显示具体百分比。
ProgressBar
的不确定模式配合文本提示(如 “正在加载数据...”),避免用户认为程序卡顿。无论数据大小是否确定,下载操作都应在后台执行,并处理异常情况(如网络中断、用户取消):
Service
配合BroadcastReceiver
或LiveData
,确保下载不随 Activity 销毁而中断。HttpURLConnection
的setRequestProperty("Range", "bytes=" + 已下载 + "-")
设置续传位置。总之,确定数据大小时用百分比进度条,不确定时用不确定模式或实时状态反馈,核心是让用户明确感知操作在进行,提升交互体验。
MVP(Model-View-Presenter)和 MVC(Model-View-Controller)是 Android 开发中常见的架构模式,两者在职责划分和交互方式上有明显差异。
MVC 模式中,Model 负责数据存储和业务逻辑,View 负责界面展示,Controller 作为中间层处理用户输入并更新 Model 和 View。但在实际开发中,View 常直接操作 Model,导致 Controller 职责模糊,形成 “Massive View” 问题。例如,Android 中的 Activity 常同时处理界面逻辑和数据交互,违背单一职责原则。
MVP 模式将 Controller 改为 Presenter,View 不再直接操作 Model,而是通过 Presenter 与 Model 交互。Presenter 持有 View 和 Model 的引用,负责数据转换和逻辑处理。例如,在登录功能中,Activity 作为 View 只处理界面渲染,Presenter 接收用户输入,调用 Model 的登录接口,并将结果返回给 View 更新界面,避免 View 与 Model 的强耦合。
维度 | MVC 模式 | MVP 模式 |
---|---|---|
交互方式 | View 可直接访问 Model,Controller 调度 | View 与 Model 无直接交互,通过 Presenter 通信 |
测试性 | View 与 Model 耦合度高,难以单元测试 | Presenter 可独立测试,View 通过接口模拟 |
职责清晰度 | Controller 职责可能重叠 | 三层职责更明确,符合单一职责原则 |
Android 适配 | Activity 常兼任 View 和 Controller | View 通常为接口,Activity 实现接口 |
MVC 的优势在于结构简单,适合小型项目,但在复杂场景下易导致代码臃肿。MVP 通过接口隔离使代码更易维护,适合中大型项目,但会增加类的数量。例如,在电商 App 的商品列表页面,MVP 模式可将数据加载、列表渲染逻辑分离到 Presenter 中,Activity 仅处理界面更新,提升代码可维护性。
观察者模式定义了对象间的一对多依赖关系,当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会得到通知并自动更新。在 Android 开发中,该模式常用于实现数据与界面的解耦和实时更新。
观察者模式的核心优势是解耦,主题和观察者无需知道对方的具体实现,仅通过接口交互。例如,在天气应用中,WeatherModel 作为主题存储天气数据,多个 Activity/Fragment 作为观察者订阅数据更新,当后台获取新数据时,自动通知所有观察者刷新界面。实现时需注意避免内存泄漏,如在 Activity 销毁时取消观察者注册,否则主题持有观察者的引用会导致 Activity 无法被回收。
装饰者模式(Decorator Pattern)属于结构型设计模式,其核心思想是动态地为对象添加新的行为,而不修改原有类的代码。它通过创建一个包装对象(装饰者)来包裹真实对象,从而在不改变原对象的情况下扩展功能。
装饰者模式包含四个角色:
以 Java 的 IO 流为例,BufferedInputStream 是 FileInputStream 的装饰者:
// 组件接口
InputStream inputStream = new FileInputStream("data.txt");
// 装饰者包装组件,添加缓冲功能
inputStream = new BufferedInputStream(inputStream);
// 继续包装,添加加密功能(假设存在加密装饰者)
inputStream = new EncryptedInputStream(inputStream);
每次包装都会添加新功能,且装饰者与组件遵循相同接口,因此可多次包装,灵活性高。
继承是静态的扩展方式,一旦子类继承父类,功能便固定;而装饰者模式是动态扩展,可在运行时选择不同装饰者组合。例如,TextView 需要同时支持红色字体和粗体,若用继承需创建 RedBoldTextView 类,但若功能更多(如下划线、阴影),继承会导致类爆炸。而装饰者模式可通过 TextDecorator 装饰者动态添加不同样式,如 RedDecorator、BoldDecorator,避免类数量膨胀。
在 SQL 中,连接(Join)用于组合两个或多个表的数据,左连接(LEFT JOIN)和右连接(RIGHT JOIN)是两种常见的连接方式,其核心区别在于保留哪张表的全部记录。
LEFT JOIN
或LEFT OUTER JOIN
,查询结果保留左表的所有记录,右表中匹配的记录会被合并,若右表无匹配记录,则对应字段为 NULL。RIGHT JOIN
或RIGHT OUTER JOIN
,保留右表的所有记录,左表中匹配的记录会被合并,无匹配时左表字段为 NULL。假设存在两张表:学生表(Student)
和课程表(Course)
,结构如下:
Student: id, name
Course: id, student_id, course_name
SELECT s.name, c.course_name
FROM Student s
LEFT JOIN Course c ON s.id = c.student_id;
course_name
为 NULL。SELECT s.name, c.course_name
FROM Student s
RIGHT JOIN Course c ON s.id = c.student_id;
name
为 NULL。连接类型 | 保留表 | NULL 出现位置 | 适用场景 |
---|---|---|---|
左连接 | 左表 | 右表不匹配的记录 | 查询所有用户及其订单(即使无订单) |
右连接 | 右表 | 左表不匹配的记录 | 查询所有订单及其用户(即使无用户) |
实际开发中,左连接更为常用,例如在 Android 的 Room 数据库中,查询所有用户及其关联的地址信息时,使用左连接可确保用户记录不丢失,地址为空时表示用户未填写地址。
算法和数据结构是计算机科学的基础,两者相互依存:数据结构定义了数据的组织形式,算法则定义了操作这些数据的方法。
数据结构 | 特点 | Android 应用场景 |
---|---|---|
数组 | 连续内存存储,随机访问高效 | ListView/RecyclerView 的适配器数据源 |
链表 | 非连续存储,插入删除高效 | 事件分发链(View 的事件传递链表) |
哈希表 | 键值对存储,查找时间复杂度 O (1) | IntentFilter 的匹配(通过哈希表快速查找) |
树 | 层级结构,如二叉树、红黑树 | Bitmap 缓存 LRU 算法(用树结构管理缓存顺序) |
算法在 Android 中也有广泛应用:
合理选择数据结构和算法可显著提升应用性能。例如,在联系人列表中,若使用数组存储联系人,查找操作需遍历所有元素(O (n)),而使用二叉搜索树(O (log n))或哈希表(O (1))可大幅提升效率。此外,Android 的布局测量过程使用递归算法遍历 View 树,若 View 层级过深会导致测量耗时,因此优化布局结构(减少层级)也是算法优化的体现。
要根据银行卡前缀查找所属银行,核心在于建立前缀与银行信息的映射关系。银行卡号的前几位(称为发卡行识别码,BIN)由国际标准化组织分配,不同银行对应特定的前缀范围。
HashMap
存储常见银行的 BIN 前缀与银行名称的映射,例如 “622202” 对应 “工商银行”。import java.util.HashMap;
import java.util.Map;
public class BankCardFinder {
// 存储银行卡前缀与银行名称的映射,按前缀长度从长到短排序存储
private final Map binBankMap;
public BankCardFinder() {
binBankMap = new HashMap<>();
// 初始化常见银行的BIN前缀(实际应用中可从数据库或配置文件加载)
binBankMap.put("622202", "中国工商银行");
binBankMap.put("622848", "中国农业银行");
binBankMap.put("622700", "中国建设银行");
binBankMap.put("621226", "中国银行");
binBankMap.put("622575", "招商银行");
binBankMap.put("621700", "中国建设银行"); // 长前缀优先存储
binBankMap.put("6222", "中国工商银行"); // 短前缀作为后备
binBankMap.put("6228", "中国农业银行");
}
/**
* 根据银行卡号查找所属银行
* @param cardNumber 银行卡号(至少包含前几位前缀)
* @return 银行名称,未找到返回null
*/
public String findBankByCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) {
return "银行卡号长度不足";
}
// 按前缀长度从长到短遍历,优先匹配长前缀
for (int length = Math.min(cardNumber.length(), 6); length >= 4; length--) {
String prefix = cardNumber.substring(0, length);
if (binBankMap.containsKey(prefix)) {
return binBankMap.get(prefix);
}
}
return "未找到对应银行";
}
/**
* 新增或更新银行前缀映射
*/
public void addBankMapping(String binPrefix, String bankName) {
binBankMap.put(binPrefix, bankName);
}
// 测试方法
public static void main(String[] args) {
BankCardFinder finder = new BankCardFinder();
System.out.println(finder.findBankByCardNumber("6222021234567890123")); // 应输出"中国工商银行"
System.out.println(finder.findBankByCardNumber("6217001234567890")); // 应输出"中国建设银行"
System.out.println(finder.findBankByCardNumber("62257512345")); // 应输出"招商银行"
}
}
JRE、JDK、JVM 是 Java 技术体系中三个核心概念,彼此关联但功能迥异,需从定义、组成和用途三个维度区分。
JVM(Java Virtual Machine,Java 虚拟机)
是 Java 程序的运行容器,负责将字节码(.class 文件)转换为机器码执行。它屏蔽了底层操作系统和硬件的差异,实现了 “一次编写,到处运行” 的跨平台特性。JVM 包含类加载器、执行引擎、内存区域等核心组件,不同平台(如 Windows、Linux、Android)有对应的 JVM 实现(如 HotSpot、Dalvik/ART)。
JRE(Java Runtime Environment,Java 运行时环境)
是运行 Java 程序所需的最小环境,包含 JVM、核心类库(如 java.lang、java.util)和其他支持文件。用户若仅需运行 Java 程序,安装 JRE 即可,无需安装开发工具。JRE 的目录结构中,lib
文件夹存放核心类库,bin
文件夹包含 Java 运行时工具(如 java.exe)。
JDK(Java Development Kit,Java 开发工具包)
是 Java 开发的完整工具集,包含 JRE 和开发所需的工具,如编译器(javac)、调试器(jdb)、文档生成工具(javadoc)等。开发者通过 JDK 编写、编译和调试 Java 程序,最终生成可在 JRE 环境中运行的字节码文件。JDK 的bin
目录下包含这些工具,lib
目录存放工具的支持库。
JDK 包含 JRE,JRE 包含 JVM。三者的依赖关系如下:
JDK → JRE → JVM
组件 | 用途 | 包含内容 | 用户群体 |
---|---|---|---|
JVM | 执行字节码,提供运行时环境 | 类加载器、执行引擎、内存管理 | 底层开发者 |
JRE | 运行 Java 程序 | JVM + 核心类库 + 运行时工具 | 普通用户 |
JDK | 开发 Java 程序 | JRE + 编译器 + 调试工具 + 开发库 | Java 开发者 |
Java 异常体系以Throwable
为根,分为Exception
和Error
两大类,其中Exception
又分为Checked Exception
和Unchecked Exception
(运行时异常)。在开发中,合理处理异常能提升程序的健壮性和用户体验。
Checked Exception(编译时异常)
try-catch
或throws
声明),否则编译失败。IOException
:文件读写、网络连接等 I/O 操作异常,如FileNotFoundException
。SQLException
:数据库操作异常,如 SQL 语法错误、连接超时。ClassNotFoundException
:类未找到异常,如动态加载类时路径错误。Unchecked Exception(运行时异常)
NullPointerException
:访问 null 对象的方法或属性,如String str = null; str.length();
。ArrayIndexOutOfBoundsException
:数组下标越界,如int[] arr = new int[5]; arr[10] = 0;
。IllegalArgumentException
:参数不合法,如传入负数作为数组长度。ClassCastException
:类型转换错误,如Object obj = "string"; int i = (int) obj;
。Error(错误)
OutOfMemoryError
)、栈溢出(StackOverflowError
),一般无需捕获,应通过优化代码或调整 JVM 参数解决。try-catch-finally 块
finally
块中的代码无论是否发生异常都会执行,常用于资源释放(如关闭文件流、数据库连接)。FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 读取文件内容
} catch (FileNotFoundException e) {
System.err.println("文件不存在");
e.printStackTrace(); // 开发阶段打印堆栈信息,便于调试
} catch (IOException e) {
System.err.println("文件读取失败");
} finally {
if (fis != null) {
try {
fis.close(); // 在finally中关闭资源,确保资源释放
} catch (IOException e) {
e.printStackTrace();
}
}
}
throws 声明
throws
关键字声明可能抛出的异常,由调用者处理。public void readFile() throws IOException {
// 可能抛出IOException的代码
}
异常包装与自定义异常
public class UserService {
public void register(String username) throws UserExistsException {
if (isUsernameExists(username)) {
throw new UserExistsException("用户名已存在");
}
// 注册逻辑
}
}
// 自定义Checked异常
class UserExistsException extends Exception {
public UserExistsException(String message) {
super(message);
}
}
catch (Exception e)
捕获所有异常,应根据异常类型分别处理,防止隐藏真正的问题。try-with-resources
语法(Java 7+)自动释放实现了AutoCloseable
接口的资源,简化代码: try (FileInputStream fis = new FileInputStream("data.txt")) {
// 读取文件,无需手动关闭fis
} catch (IOException e) {
// 处理异常
}
ImageLoader 是 Android 开发中加载和管理图片的核心组件,需解决异步加载、内存缓存、磁盘缓存、图片压缩等关键问题,避免 OOM 和 UI 卡顿。以下从架构设计和核心功能两方面说明实现思路。
ExecutorService
)管理图片加载任务,避免创建大量临时线程。Android 中可结合HandlerThread
或IntentService
处理后台任务。LruCache
(最近最少使用算法)存储常用图片,根据应用内存情况设置缓存大小(如Runtime.getRuntime().maxMemory() / 8
)。DiskLruCache
存储内存中未命中的图片,减少网络请求或文件读取频率。BitmapFactory.Options
设置inSampleSize
,避免加载全尺寸图片导致内存溢出。Handler
或Callback
将加载完成的图片传递给 UI 线程,更新 ImageView。内存缓存的实现
// 基于LruCache实现内存缓存
private LruCache mMemoryCache;
public void initMemoryCache() {
// 获取最大可用内存的1/8作为缓存大小
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 返回Bitmap的占用内存大小(单位:KB)
return bitmap.getByteCount() / 1024;
}
};
}
public void addToMemoryCache(String key, Bitmap bitmap) {
if (getFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
磁盘缓存的实现
// 基于DiskLruCache实现磁盘缓存(需处理IOException)
private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static final String DISK_CACHE_DIR = "image_cache";
public void initDiskCache(Context context) {
File cacheDir = new File(context.getCacheDir(), DISK_CACHE_DIR);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
try {
mDiskCache = DiskLruCache.open(cacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
public void addToDiskCache(String key, Bitmap bitmap) {
if (mDiskCache == null) return;
String filename = key.hashCode() + ".jpg";
try {
DiskLruCache.Editor editor = mDiskCache.edit(filename);
if (editor != null) {
OutputStream os = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, os);
editor.commit();
mDiskCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public Bitmap getFromDiskCache(String key) {
if (mDiskCache == null) return null;
String filename = key.hashCode() + ".jpg";
try {
DiskLruCache.Snapshot snapshot = mDiskCache.get(filename);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
return BitmapFactory.decodeStream(is);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
图片加载与异步处理
使用Handler
或AsyncTask
在后台线程加载图片,完成后切换到主线程更新 UI:
// 简化的图片加载方法
public void displayImage(final String imageUrl, final ImageView imageView) {
// 先从内存缓存获取
String key = imageUrl;
Bitmap bitmap = getFromMemoryCache(key);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
// 内存未命中,从磁盘缓存获取
bitmap = getFromDiskCache(key);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
addToMemoryCache(key, bitmap);
return;
}
// 磁盘未命中,发起网络请求(实际需结合OkHttp或Volley)
imageView.setImageResource(R.drawable.place_holder); // 设置占位图
loadImageFromNetwork(imageUrl, imageView, key);
}
private void loadImageFromNetwork(final String url, final ImageView imageView, final String key) {
// 使用线程池加载图片
new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟网络请求获取图片(实际需用OkHttp等库)
Bitmap bitmap = downloadBitmap(url);
if (bitmap != null) {
// 压缩图片
bitmap = compressBitmap(bitmap, imageView.getWidth(), imageView.getHeight());
// 存入缓存
addToMemoryCache(key, bitmap);
addToDiskCache(key, bitmap);
// 更新UI(通过Handler切换到主线程)
mHandler.post(new Runnable() {
@Override
public void run() {
ImageView currentView = mImageViewMap.get(key);
if (currentView == imageView && bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
BitmapFactory.Options.inJustDecodeBounds
先获取图片原始尺寸。ScrollListener
监听滑动状态。WeakReference
)存储 ImageView,防止 Activity 销毁后图片任务仍持有引用。实际开发中,可参考成熟的开源库(如 Glide、Picasso、Fresco)的实现原理,这些库已处理了图片加载的复杂场景,如动图支持、圆角裁剪、高斯模糊等,比自定义实现更高效和稳定。