rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • 滴滴 面试

  • 说一下抽象类和接口的区别?抽象类和接口可以有方法体吗?Java 可以多实现或者多继承吗?
  • 多态和重载区别是什么?
  • 同一个锁为什么效率有差别?
  • == 和 equals 的区别?
  • 深拷贝和浅拷贝的区别?
  • HTTP 和 HTTPS 的区别是什么?对称加密和非对称加密有哪些?说一些常见网络错误码?
  • UNICODE 和 utf-8 是干什么的,一个中文分别在其中占据多少大小?
  • content-type 有哪些值?常见的响应码有哪些?
  • ArrayList 和 LinkedList 的区别?LinkedList 是双向链表吗?
  • HashMap 和 HashTable 的区别?
  • HashMap 的底层实现是怎样的?哈希冲突如何解决?HashMap 什么时候进行扩容?
  • 顺序的数组,插入一个元素(二分)
  • 用两个队列实现栈,需要线程安全吗?
  • 说一说你熟悉的几个设计模式?
  • 单例模式的分类?单例模式的线程安全如何保证?
  • 观察者模式是什么?怎么用?
  • MVP 架构是怎样的?
  • 设计模式:观察者模式和 *** 模式区别
  • 如何保证线程安全?synchronized 关键字的作用是什么?lock 各种原理是什么?
  • 多线程的实现方法有哪些?
  • Handler 机制是怎样的?
  • Looper 是线程唯一的吗?
  • 主线程的 Looper 的创建时机是什么?
  • 如何保证主线程不阻塞?
  • 讲一下消息机制,怎么实现一个线程只有一个 Looper
  • 现在 push 一个延迟消息到消息队列里,这时候忽然退出应用程序了,会有什么问题?
  • View 的事件分发机制是怎样的?
  • 解决过滑动冲突吗?
  • 自定义过 View 吗?如何实现的?
  • 触发 invalidate () 和 requestLayout () 会发生什么?
  • 现在需要设置 RecyclerView 的每个 item 都不一样如何实现?
  • Activity 有哪几种启动模式?Activity 界面跳转解耦的方法有哪些?
  • Fragment 的生命周期是怎样的?
  • Service 的启动方式有哪些?有什么区别?
  • 了解 Binder 吗?
  • 项目经历介绍,遇到的问题及解决方法
  • Android 性能优化这块,在项目中有获得什么经验吗?比如大图加载问题怎么解决?Glide 缓存的原理是什么?
  • Android 缓存如何缓存,图片如何缓存,数据如何缓存,缓存机制是什么?
  • js 和 android 耦合是怎么样的?
  • React Native 有什么了解?知道 React Native 有什么不好的地方吗?
  • Kotlin 以及对其的看法
  • Flutter 有没有用过?在 Flutter 里 streams 是什么?stream 有几种订阅模式,如何调用?Flutter 的绘制流程是怎样的?
  • 如何解决内存泄漏?
  • oom 是什么,应该怎么优化?
  • 怎么定位 ANR?
  • 对滴滴印象如何?
  • 部门技术栈是什么?Kotlin 语言为主,有一套自己设计的架构,对此有何看法?
  • 对第一份工作有什么期许?
  • 聊聊实习中做的东西,提出一些问题,并解答这些问题
  • 聊一个实际场景,activity 的生命周期是如何调用的
  • Spring 的 bean 加载顺序是怎样的?
  • 进程线程的抽象理解是怎样的?
  • 操作系统层面的理解有哪些?
  • 进程之间通信是如何实现的?
  • AMS 是如何启动的?AMS 在 Android 起到什么作用?AMS 有哪些应用场景?我们是如何应用 AMS 核心原理的?
  • WMS 的工作原理说说?
  • JVM 的核心原理懂多少?JVM 的内存划分是怎样的?
  • 如何批量发布?
  • 应用崩溃了怎么办,如何收集崩溃信息?
  • 应用网络不好如何判断?
  • 通信如何保证安全?
  • 计算机网络:socket、http 与操作系统层面的处理
  • 进程与线程的区别
  • 类加载器分类。具体的执行次序
  • 接口和类的区别
  • equal 和 hashcode
  • 手撕冒泡排序
  • 算法:洗牌不回到原来位置
    • 一个字符串交换顺序

滴滴 面试

说一下抽象类和接口的区别?抽象类和接口可以有方法体吗?Java 可以多实现或者多继承吗?

  • 抽象类和接口的区别:
    • 语法层面:抽象类使用 abstract 关键字修饰,其中可以包含抽象方法和非抽象方法;接口使用 interface 关键字定义,其方法默认都是抽象的,JDK8 开始可以有默认方法和静态方法。
    • 实现方式:一个类只能继承一个抽象类,通过 extends 关键字;而一个类可以实现多个接口,使用 implements 关键字。
    • 设计目的:抽象类主要用于对一组具有相似特征和行为的类进行抽象,作为它们的基类,体现的是一种 “is-a” 的关系;接口则更多地用于定义一组规范和契约,规定实现类必须提供的方法和行为,体现的是一种 “like-a” 的关系。
  • 抽象类和接口中的方法体:抽象类中的抽象方法没有方法体,只有方法签名,需要子类去实现;非抽象方法可以有方法体。接口中的抽象方法默认没有方法体,但从 JDK8 开始,接口中的默认方法和静态方法可以有方法体。
  • Java 中的多实现和多继承:Java 中类只能单继承,不能多继承,以避免类层次结构的复杂性和潜在的冲突;但一个类可以实现多个接口,从而实现多态和代码的复用。

多态和重载区别是什么?

  • 概念本质:多态是指同一个行为具有多个不同表现形式或形态的能力,是在运行时根据对象的实际类型来决定调用哪个方法;重载是指在同一个类中,允许存在多个同名方法,但是它们的参数列表不同,与返回值类型无关,是在编译时根据参数的类型和数量来确定调用哪个方法。
  • 实现方式:多态的实现依赖于继承、接口实现和方法重写,通过在子类中重写父类的方法,使得不同的子类对象对同一方法调用有不同的实现;重载则是在同一个类中定义多个同名方法,通过不同的参数列表来区分。
  • 调用时机:多态是在运行时确定具体调用哪个方法,根据对象的实际类型动态绑定;重载是在编译时确定调用的方法,编译器根据调用时传递的参数类型和数量来选择合适的方法。

同一个锁为什么效率有差别?

  • 锁的竞争程度:如果多个线程同时竞争同一个锁,那么就会导致线程频繁地阻塞和唤醒,这会带来较大的开销,从而降低效率;而如果锁的竞争较小,线程能够较快地获取到锁并执行临界区代码,效率相对较高。
  • 锁的粒度:锁的粒度指的是被锁保护的代码块的大小。如果锁的粒度较大,那么持有锁的时间就会较长,其他线程等待锁的时间也会相应增加,从而降低效率;相反,如果锁的粒度较小,只对关键部分进行加锁,那么可以减少线程等待的时间,提高效率。
  • 锁的实现机制:不同的锁实现机制在性能上也会有差异。例如,Java 中的 synchronized 关键字是一种重量级锁,在高并发场景下可能会导致性能问题;而一些轻量级锁如 ReentrantLock 等,在某些情况下可能具有更好的性能,因为它们可以更灵活地控制锁的获取和释放,减少不必要的阻塞和唤醒操作。

== 和 equals 的区别?

  • 比较的内容:“==” 比较的是两个变量所指向的内存地址是否相同,即是否是同一个对象;“equals” 方法在默认情况下,比较的是两个对象的引用是否相等,但在很多类中被重写,用于比较对象的内容是否相等。
  • 使用场景:“==” 通常用于比较基本数据类型的值是否相等,或者判断两个对象是否是同一个实例;“equals” 方法主要用于比较两个对象的内容是否相等,特别是在自定义类中,需要根据具体的业务逻辑来判断两个对象是否相等时,通常会重写 “equals” 方法。
  • 可重写性:“==” 是一个运算符,其行为不能被重写;“equals” 是 Object 类中的一个方法,可以在子类中根据需要进行重写,以实现自定义的相等比较逻辑。

深拷贝和浅拷贝的区别?

  • 拷贝的深度:浅拷贝只是对对象的引用进行拷贝,新对象和原对象共享内部的子对象;深拷贝则是对对象及其所有子对象进行完全的拷贝,新对象和原对象及其子对象都不共享任何内存空间。
  • 内存分配:浅拷贝在拷贝时,只是增加了一个指向原对象内存空间的引用,不会为新对象分配新的内存空间;深拷贝会为新对象及其所有子对象重新分配内存空间,将原对象及其子对象的内容完全复制到新的内存空间中。
  • 对原对象的影响:浅拷贝后,对新对象的修改可能会影响到原对象,因为它们共享部分内存空间;深拷贝后,新对象和原对象是完全独立的,对新对象的修改不会影响原对象。

HTTP 和 HTTPS 的区别是什么?对称加密和非对称加密有哪些?说一些常见网络错误码?

  • HTTP 和 HTTPS 的区别:
    • 安全性:HTTP 是超文本传输协议,数据以明文形式传输,这使得在传输过程中数据容易被窃取、篡改或监听;HTTPS 则是在 HTTP 的基础上加入了 SSL/TLS 协议,对数据进行加密处理,使得数据传输更加安全。
    • 连接方式:HTTP 连接相对简单,客户端向服务器发送请求,服务器响应请求;HTTPS 在连接时需要先进行 SSL/TLS 握手,以验证服务器的身份和协商加密算法等参数,然后再进行数据传输。
    • 端口:HTTP 默认使用 80 端口进行数据传输;HTTPS 默认使用 443 端口。
    • 证书:HTTPS 需要服务器拥有由权威证书颁发机构颁发的数字证书,客户端可以通过验证证书来确认服务器的身份是否可信;HTTP 则不需要证书。
  • 对称加密和非对称加密:
    • 对称加密:指加密和解密使用相同的密钥,常见的对称加密算法有 DES、3DES、AES 等。其优点是加密速度快,效率高,适合对大量数据进行加密;缺点是密钥管理困难,因为需要在通信双方之间安全地共享密钥。
    • 非对称加密:使用一对密钥,即公钥和私钥,公钥可以公开,用于加密数据,私钥则由持有者保密,用于解密数据,常见的非对称加密算法有 RSA、DSA、ECC 等。其优点是安全性高,密钥管理相对简单;缺点是加密和解密速度较慢,不适合对大量数据进行加密。
  • 常见网络错误码:
    • 400 Bad Request:表示客户端请求的语法错误,服务器无法理解。通常是由于请求参数错误、格式不正确或缺少必要的参数等原因导致。
    • 401 Unauthorized:表示客户端请求需要用户身份验证,但客户端未提供有效的身份验证信息或身份验证失败。
    • 403 Forbidden:表示服务器理解客户端的请求,但拒绝执行该请求,通常是由于客户端没有访问权限或资源受到限制等原因导致。
    • 404 Not Found:表示客户端请求的资源在服务器上未找到,通常是由于请求的 URL 路径错误或资源已被删除等原因导致。
    • 500 Internal Server Error:表示服务器在处理客户端请求时发生了内部错误,通常是由于服务器端的代码错误、数据库故障或其他服务器端的问题导致。

UNICODE 和 utf-8 是干什么的,一个中文分别在其中占据多少大小?

  • UNICODE 的作用:UNICODE 是一种字符编码标准,它为世界上几乎所有的字符都分配了一个唯一的数字编号,即码点,目的是解决不同字符集之间的编码冲突和不兼容问题,使得不同的计算机系统和应用程序能够统一地表示和处理各种文字和符号,实现全球范围内的文本信息交换和共享。
  • UTF-8 的作用:UTF-8 是一种可变长度的字符编码方式,它是 UNICODE 的一种实现方式。UTF-8 使用 1 到 4 个字节来表示一个 UNICODE 字符,根据字符的不同而采用不同长度的编码,对于 ASCII 字符,UTF-8 使用 1 个字节表示,与 ASCII 编码完全兼容;对于其他字符,根据其 UNICODE 码点的范围,使用 2 个、3 个或 4 个字节表示,这样可以有效地节省存储空间,同时也便于网络传输和处理。
  • 一个中文在其中占据的大小:在 UNICODE 中,一个中文通常占据 2 个字节或 4 个字节,具体取决于所使用的 UNICODE 版本和编码方式。在 UTF-8 中,一个中文通常占据 3 个字节,但在某些特殊情况下,可能会使用 4 个字节表示。

content-type 有哪些值?常见的响应码有哪些?

  • content-type 常见的值:
    • text/html:表示 HTML 格式的文本内容,常用于网页的响应。
    • text/plain:表示纯文本内容,通常用于简单的文本文件或文本消息的传输。
    • application/json:表示 JSON 格式的数据,常用于前后端数据交互,以传递结构化的数据。
    • application/xml:表示 XML 格式的数据,常用于数据交换和配置文件等。
    • application/pdf:表示 PDF 文档,用于在网络上传输 PDF 文件。
    • image/jpeg:表示 JPEG 图像格式,用于传输 JPEG 图片。
    • image/png:表示 PNG 图像格式,用于传输 PNG 图片。
    • audio/mpeg:表示 MP3 音频格式,用于传输 MP3 音频文件。
    • video/mp4:表示 MP4 视频格式,用于传输 MP4 视频文件。
  • 常见的响应码:
    • 200 OK:表示请求成功,服务器成功处理了客户端的请求,并返回了所请求的资源或数据。
    • 201 Created:表示请求成功并且服务器创建了新的资源,通常在客户端向服务器提交数据并成功创建新的记录或对象时返回。
    • 204 No Content:表示请求成功,但服务器没有返回任何内容,通常在客户端执行删除或更新操作成功,但不需要返回具体数据时使用。
    • 301 Moved Permanently:表示所请求的资源已被永久移动到新的位置,客户端应使用新的 URL 重新发送请求。
    • 302 Found:表示所请求的资源临时移动到了其他位置,客户端应使用新的 URL 重新发送请求,但在未来的请求中可能会再次改变位置。
    • 304 Not Modified:表示客户端发送的请求条件与服务器上的资源状态相符,服务器返回此状态码告知客户端可以使用本地缓存的资源,无需再次从服务器获取。
    • 400 Bad Request:表示客户端请求的语法错误,服务器无法理解。通常是由于请求参数错误、格式不正确或缺少必要的参数等原因导致。
    • 401 Unauthorized:表示客户端请求需要用户身份验证,但客户端未提供有效的身份验证信息或身份验证失败。
    • 403 Forbidden:表示服务器理解客户端的请求,但拒绝执行该请求,通常是由于客户端没有访问权限或资源受到限制等原因导致。
    • 404 Not Found:表示客户端请求的资源在服务器上未找到,通常是由于请求的 URL 路径错误或资源已被删除等原因导致。
    • 500 Internal Server Error:表示服务器在处理客户端请求时发生了内部错误,通常是由于服务器端的代码错误、数据库故障或其他服务器端的问题导致。
    • 503 Service Unavailable:表示服务器暂时无法处理客户端的请求,通常是由于服务器过载、维护或其他临时故障导致。

ArrayList 和 LinkedList 的区别?LinkedList 是双向链表吗?

  • ArrayList 和 LinkedList 的区别:
    • 数据结构:ArrayList 是基于数组实现的动态数组,它在内存中是连续存储的;LinkedList 是基于双向链表实现的数据结构,每个节点包含数据和指向前一个节点和后一个节点的引用。
    • 访问速度:ArrayList 通过索引访问元素的速度非常快,时间复杂度为 O (1);LinkedList 需要遍历链表来访问元素,访问特定位置的元素时间复杂度为 O (n),其中 n 是链表的长度。
    • 插入和删除操作:ArrayList 在插入和删除元素时,如果是在末尾操作,速度较快,时间复杂度为 O (1),但如果是在中间或开头插入和删除元素,需要移动大量元素,时间复杂度为 O (n);LinkedList 在插入和删除元素时,只需要修改节点的引用,时间复杂度为 O (1),但如果要先找到插入或删除的位置,需要遍历链表,时间复杂度为 O (n)。
    • 内存占用:ArrayList 在初始化时会分配一定大小的数组空间,如果元素数量较少,可能会浪费一些内存;LinkedList 每个节点都需要额外的空间来存储指向前一个节点和后一个节点的引用,所以在存储相同数量的元素时,LinkedList 通常比 ArrayList 占用更多的内存。
  • LinkedList 是双向链表吗:LinkedList 是双向链表,它的每个节点都包含对前一个节点和后一个节点的引用,这使得在链表中可以方便地进行向前和向后的遍历操作,以及在任意位置进行插入和删除操作。

HashMap 和 HashTable 的区别?

  • 线程安全性:HashMap 是非线程安全的,在多线程环境下可能会出现并发问题,如数据不一致、死锁等;HashTable 是线程安全的,它的所有方法都使用了 synchronized 关键字进行同步,保证在多线程环境下数据的一致性和完整性。
  • null 值和 null 键:HashMap 允许键和值为 null,其中最多只能有一个键为 null,但是可以有多个值为 null;HashTable 不允许键和值为 null,否则会抛出 NullPointerException 异常。
  • 性能:由于 HashMap 没有同步机制,在单线程环境下,它的性能通常比 HashTable 要好,因为不需要进行额外的同步操作,访问和操作数据的速度更快;在多线程环境下,如果不需要考虑线程安全问题,使用 HashMap 可以提高程序的性能,而如果需要保证线程安全,HashTable 的性能会因为同步机制而受到一定的影响。
  • 初始容量和扩容机制:HashMap 的默认初始容量是 16,加载因子是 0.75,当元素数量超过初始容量乘以加载因子时,会进行扩容,扩容后的容量是原来的 2 倍;HashTable 的默认初始容量是 11,加载因子是 0.75,当元素数量超过初始容量乘以加载因子时,会进行扩容,扩容后的容量是原来的 2 倍加 1。
  • 遍历方式:HashMap 可以使用迭代器、增强 for 循环、Lambda 表达式等方式进行遍历;HashTable 可以使用迭代器、枚举等方式进行遍历,但是由于 HashTable 的方法都是同步的,在遍历过程中如果其他线程对 HashTable 进行修改,可能会导致 ConcurrentModificationException 异常。

HashMap 的底层实现是怎样的?哈希冲突如何解决?HashMap 什么时候进行扩容?

HashMap 的底层实现主要基于数组和链表或红黑树。它通过对键进行哈希运算,得到一个哈希值,然后根据这个哈希值确定键值对在数组中的存储位置。当发生哈希冲突时,即不同的键计算出的哈希值相同时,会将新的键值对以链表的形式存储在该位置的链表中。在 Java 8 中,如果链表的长度超过了一定阈值(默认为 8),并且数组的长度大于等于 64,链表会转换为红黑树,以提高查找效率。

哈希冲突的解决主要有两种方式,一种是开放定址法,另一种是链地址法,HashMap 采用的是链地址法,即将冲突的元素以链表的形式存储在同一个桶中。当链表长度过长时,会转换为红黑树来优化查询性能。

HashMap 在元素数量达到一定阈值时会进行扩容。当 HashMap 中的元素个数超过了数组大小乘以负载因子(默认负载因子为 0.75)时,就会触发扩容操作。扩容时会创建一个新的数组,其大小通常为原数组的两倍,然后将原数组中的元素重新哈希到新数组中。

顺序的数组,插入一个元素(二分)

对于有序数组插入一个元素,可以使用二分查找来确定插入位置,以减少比较次数,提高插入效率。以下是基本思路:

首先,使用二分查找算法在有序数组中找到要插入元素的合适位置。在二分查找过程中,不断比较要插入的元素与中间元素的大小,如果要插入的元素小于中间元素,则在左半部分继续查找;如果大于中间元素,则在右半部分继续查找;如果相等,则可以根据具体需求决定插入位置,一般可以插入在相等元素的后面。找到合适位置后,将该位置及其后面的元素依次向后移动一位,然后将待插入元素插入到该位置。

例如,有一个有序数组 [1, 3, 5, 7, 9],要插入元素 4。首先,通过二分查找找到 3 和 5 之间的位置,然后将 5 及其后面的元素 7、9 依次向后移动一位,最后将 4 插入到 3 后面的位置,得到新的有序数组 [1, 3, 4, 5, 7, 9]。

用两个队列实现栈,需要线程安全吗?

用两个队列实现栈时,是否需要线程安全取决于具体的应用场景。如果在多线程环境下使用该栈,并且多个线程可能同时对栈进行操作,那么就需要考虑线程安全问题;如果只是在单线程环境中使用,则不需要考虑线程安全。

在多线程环境中,如果不保证线程安全,可能会导致数据不一致、栈状态错误等问题。例如,当一个线程正在进行入栈操作,而另一个线程同时进行出栈操作,可能会出现并发访问冲突,导致栈的结构被破坏,数据丢失或错误。

为了实现线程安全,可以使用同步机制,如在操作队列的方法上添加 synchronized 关键字,或者使用更高级的并发工具类,如 ReentrantLock、BlockingQueue 等,来确保在同一时刻只有一个线程能够访问和修改队列,从而保证栈的正确操作和数据的一致性。

说一说你熟悉的几个设计模式?

常见的设计模式有以下几种:

  • 单例模式:确保一个类只有一个实例,并提供一个全局访问点。例如,在 Windows 系统中的任务管理器,无论在系统的任何地方调用它,都是同一个实例。单例模式可以节省系统资源,避免频繁创建和销毁对象。
  • 工厂模式:将对象的创建和使用分离,通过一个工厂类来负责创建对象。比如在游戏开发中,创建不同类型的游戏角色,可以使用工厂模式,根据不同的需求创建不同的角色实例,提高代码的可维护性和可扩展性。
  • 观察者模式:定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。例如,在社交媒体平台上,当一个用户发布了一条新动态,关注他的其他用户会收到通知,这就是观察者模式的应用。
  • 策略模式:定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。在电商系统中,不同的促销活动可以看作是不同的策略,根据不同的促销规则计算商品的价格,方便在不修改原有代码的基础上增加新的促销策略。

单例模式的分类?单例模式的线程安全如何保证?

单例模式主要有以下几种分类:

  • 饿汉式单例:在类加载时就创建单例对象,优点是实现简单,线程安全,缺点是如果单例对象在程序运行过程中一直未被使用,会造成资源浪费。例如:
public class Singleton {
    private static Singleton instance = new Singleton();
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}
  • 懒汉式单例:在第一次调用 getInstance 方法时才创建单例对象,优点是可以延迟加载,节省资源,但是在多线程环境下需要考虑线程安全问题。例如:
public class Singleton {
    private static Singleton instance;
 
    private Singleton() {}
 
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 双重检查锁单例:在懒汉式单例的基础上,通过双重检查锁机制来提高性能并保证线程安全。例如:
public class Singleton {
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 静态内部类单例:利用静态内部类的特性来实现单例,既保证了线程安全,又实现了延迟加载。例如:
public class Singleton {
    private Singleton() {}
 
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
 
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
  • 枚举单例:通过枚举类型来实现单例,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。例如:
public enum Singleton {
    INSTANCE;
 
    public void doSomething() {
        // 单例对象的方法
    }
}

在多线程环境下,保证单例模式的线程安全可以采用以下几种方法:

  • 使用 synchronized 关键字:在获取单例对象的方法上添加 synchronized 关键字,如懒汉式单例中的 getInstance 方法,这样可以保证在同一时刻只有一个线程能够访问该方法,从而避免多个线程同时创建单例对象。但是这种方式会降低性能,因为每次调用该方法都需要获取锁。
  • 双重检查锁机制:在懒汉式单例的基础上,通过双重检查锁机制来减少同步的开销。在第一次检查实例是否为 null 时,如果不为 null,则直接返回实例,不需要获取锁;只有在第一次检查为 null 时,才进入同步块再次检查并创建实例。同时,需要将实例变量声明为 volatile,以保证可见性和禁止指令重排序。
  • 静态内部类:利用静态内部类的特性,在类加载时不会立即初始化内部类,只有在第一次调用 getInstance 方法时,才会加载并初始化内部类,从而创建单例对象。由于类加载过程是线程安全的,所以这种方式可以保证单例的线程安全,并且实现了延迟加载。
  • 枚举:枚举类型在 Java 中是天然的单例,因为 Java 虚拟机在加载枚举类时会保证只有一个实例存在,并且枚举类的实例是线程安全的,所以可以通过枚举来实现单例模式,避免了复杂的线程安全处理。

观察者模式是什么?怎么用?

观察者模式是一种软件设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

在 Java 中,观察者模式通常由抽象主题角色、具体主题角色、抽象观察者角色和具体观察者角色组成。抽象主题角色定义了添加、删除和通知观察者的方法;具体主题角色实现了抽象主题角色的方法,并在状态发生变化时通知观察者;抽象观察者角色定义了更新方法;具体观察者角色实现了抽象观察者角色的更新方法,并在收到通知时更新自己的状态。

例如,在一个新闻发布系统中,新闻发布者是主题对象,订阅新闻的用户是观察者对象。当新闻发布者发布一条新新闻时,会通知所有订阅者,订阅者收到通知后可以更新自己的界面显示新新闻。

MVP 架构是怎样的?

MVP 架构即 Model-View-Presenter 架构,是一种用于分离应用程序的业务逻辑、用户界面和数据存储的设计模式。

  • Model:负责处理数据的获取、存储和业务逻辑的实现,通常与数据库、网络请求等数据源进行交互。例如,在一个新闻应用中,Model 层负责从服务器获取新闻数据,并对数据进行处理和存储。
  • View:负责显示用户界面,向用户展示数据,并接收用户的交互操作。它通常是一个 Activity 或 Fragment,只负责展示数据和接收用户输入,不处理业务逻辑。例如,在新闻应用中,View 层负责显示新闻列表和新闻详情页面。
  • Presenter:作为 Model 和 View 之间的桥梁,负责处理用户交互逻辑和业务逻辑,并将处理结果反馈给 View 层。它从 Model 层获取数据,并将数据传递给 View 层进行显示,同时接收 View 层的用户操作,并调用 Model 层的方法进行处理。例如,在新闻应用中,Presenter 层负责处理新闻列表的加载、新闻详情的获取和用户的点赞、评论等操作。

设计模式:观察者模式和 *** 模式区别

由于你未明确指出与观察者模式对比的具体是哪种模式,这里以工厂模式为例进行说明。

  • 目的不同:观察者模式的主要目的是在对象之间建立一种一对多的依赖关系,当一个对象的状态发生变化时,能够自动通知其他依赖它的对象;而工厂模式的主要目的是创建对象,将对象的创建和使用分离,提高代码的可维护性和可扩展性。
  • 结构不同:观察者模式主要由观察者和被观察者组成,被观察者维护一个观察者列表,当自身状态发生变化时,遍历列表通知观察者;工厂模式主要由工厂类和产品类组成,工厂类负责创建产品类的对象,客户端通过调用工厂类的方法获取产品对象。
  • 应用场景不同:观察者模式适用于当一个对象的状态变化需要通知其他多个对象的场景,如消息推送、事件监听等;工厂模式适用于对象的创建过程比较复杂,或者需要根据不同的条件创建不同类型的对象的场景,如数据库连接池、游戏角色创建等。

如何保证线程安全?synchronized 关键字的作用是什么?lock 各种原理是什么?

保证线程安全的方法有多种,常见的有使用同步机制、互斥锁、信号量、原子操作等。

  • synchronized 关键字:它可以修饰方法或代码块,用于保证在同一时刻,只有一个线程能够访问被修饰的方法或代码块。当一个线程访问被 synchronized 修饰的方法或代码块时,其他线程必须等待该线程执行完毕后才能访问。例如,在一个多线程环境下,多个线程同时访问一个共享资源,如果不使用 synchronized 关键字进行同步,可能会导致数据不一致的问题。
  • Lock 接口:它提供了比 synchronized 关键字更灵活的锁机制。Lock 接口的实现类如 ReentrantLock,通过显式地获取和释放锁来实现线程同步。它支持可重入锁、公平锁和非公平锁等多种锁机制,可以根据具体的应用场景选择合适的锁机制。例如,在一个高并发的系统中,如果需要实现公平的锁获取机制,可以使用 ReentrantLock 的公平锁模式。

多线程的实现方法有哪些?

在 Java 中,实现多线程主要有以下几种方法:

  • 继承 Thread 类:通过创建一个继承自 Thread 类的子类,并重写 run () 方法来定义线程的执行逻辑。然后创建该子类的实例,并调用 start () 方法启动线程。例如,以下代码创建了一个简单的线程类:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程正在运行");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  • 实现 Runnable 接口:创建一个实现 Runnable 接口的类,并重写 run () 方法。然后创建该类的实例,并将其作为参数传递给 Thread 类的构造函数,最后调用 start () 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程正在运行");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
  • 使用 Callable 和 Future 接口:Callable 接口类似于 Runnable 接口,但它可以返回一个结果。Future 接口用于获取 Callable 任务的结果。可以通过创建一个实现 Callable 接口的类,并重写 call () 方法来定义线程的执行逻辑和返回结果。然后使用 ExecutorService 提交 Callable 任务,并通过 Future 获取任务的结果。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1 + 1;
    }
}
 
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(new MyCallable());
        Integer result = future.get();
        System.out.println("结果:" + result);
        executor.shutdown();
    }
}
  • 使用线程池:线程池可以管理和复用线程,减少线程创建和销毁的开销。可以通过使用 ExecutorService 接口和其实现类来创建线程池,并提交任务到线程池中执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                System.out.println("线程正在运行");
            });
        }
        executor.shutdown();
    }
}

Handler 机制是怎样的?

Handler 机制主要用于在 Android 中实现线程间的通信。它主要涉及到四个核心组件:Message、Handler、MessageQueue 和 Looper。

  • Message:用于在线程间传递数据和消息,包含了消息的 what、obj 等属性,可以携带各种类型的数据。
  • Handler:主要负责发送和处理消息。它可以将消息发送到指定的消息队列中,也可以在消息队列中获取并处理消息。
  • MessageQueue:是一个消息队列,用于存储和管理消息。它按照消息的发送时间顺序排列消息,先进先出。
  • Looper:负责不断地从消息队列中取出消息,并将消息分发给对应的 Handler 进行处理。每个线程只有一个 Looper,它会一直循环遍历消息队列,直到消息队列中没有消息为止。

当在一个线程中创建了 Handler 后,它会与该线程的 Looper 和 MessageQueue 关联起来。通过 Handler 的 sendMessage () 等方法可以将消息发送到消息队列中,Looper 会不断地从消息队列中取出消息,并将消息分发给对应的 Handler 进行处理。

Looper 是线程唯一的吗?

通常情况下,一个线程只有一个 Looper。因为 Looper 的主要作用是循环遍历消息队列,一个线程只需要一个这样的循环机制来处理消息。但从技术上来说,可以在一个线程中创建多个 Looper,但这种情况比较少见且不推荐,因为这可能会导致消息处理的混乱和复杂性增加。

主线程的 Looper 的创建时机是什么?

在 Android 应用启动时,当 ActivityThread 的 main () 方法被调用时,会创建主线程的 Looper。在 main () 方法中,会通过 Looper.prepareMainLooper () 方法来创建主线程的 Looper,并通过 Looper.loop () 方法开启消息循环。这使得主线程能够不断地接收和处理各种系统和应用发出的消息,如用户的点击事件、系统的广播等。

如何保证主线程不阻塞?

  • 合理使用异步任务:对于耗时的操作,如网络请求、文件读取等,应该放在异步任务中执行,避免在主线程中直接进行这些耗时操作,以免阻塞主线程。
  • 优化布局和绘制:尽量减少布局的嵌套和复杂度过高的绘制操作,避免在主线程中进行大量的布局计算和绘制,以减少主线程的负担。
  • 及时处理消息:在主线程的消息处理中,要确保及时处理消息,避免在消息处理方法中进行耗时操作,以免阻塞后续消息的处理。

讲一下消息机制,怎么实现一个线程只有一个 Looper

消息机制主要由上述的 Message、Handler、MessageQueue 和 Looper 四个部分组成,其核心是 Looper 不断从 MessageQueue 中取出消息,然后分发给对应的 Handler 进行处理。

要实现一个线程只有一个 Looper,可以在 Looper 的内部使用一个 ThreadLocal 来存储当前线程的 Looper 实例。在 Looper 的 prepare () 方法中,首先检查当前线程是否已经有 Looper 实例,如果有则抛出异常,否则创建一个新的 Looper 实例并存储到 ThreadLocal 中。这样就可以保证在一个线程中只能创建一个 Looper 实例。具体代码如下:

public class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();
 
    public static void prepare() {
        if (sThreadLocal.get()!= null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper());
    }
 
    public static Looper myLooper() {
        return sThreadLocal.get();
    }
}

现在 push 一个延迟消息到消息队列里,这时候忽然退出应用程序了,会有什么问题?

当把一个延迟消息推送到消息队列后突然退出应用程序,可能会出现以下几种情况和问题:

  • 消息丢失:如果在延迟消息还未被处理之前应用程序就退出了,那么该消息将不会被处理,可能会导致一些业务逻辑无法正常完成。
  • 资源未及时释放:如果在消息队列中还有未处理的消息,尤其是那些与外部资源相关的消息,如网络连接、文件操作等,可能会导致资源无法及时释放,从而造成资源浪费和潜在的内存泄漏等问题。
  • 界面更新异常:如果延迟消息是用于更新界面的,那么应用程序退出后,可能会导致界面更新不完整或出现异常,给用户留下不好的体验。
  • 潜在的崩溃风险:如果在消息处理的回调方法中有一些未捕获的异常,并且应用程序突然退出,可能会导致这些异常无法被正确处理,从而增加了应用程序崩溃的风险。

View 的事件分发机制是怎样的?

View 的事件分发机制主要涉及三个重要的方法:dispatchTouchEvent ()、onInterceptTouchEvent () 和 onTouchEvent ()。

  • dispatchTouchEvent():是事件分发的入口方法,用于将事件分发给当前 View 或其子 View。它会首先调用 onInterceptTouchEvent () 方法来判断是否拦截事件,如果不拦截则会将事件分发给子 View 的 dispatchTouchEvent () 方法,否则将事件交给自身的 onTouchEvent () 方法处理。
  • onInterceptTouchEvent():用于判断当前 View 是否拦截事件,只有 ViewGroup 才有这个方法,默认返回 false,即不拦截事件。如果返回 true,则表示当前 View 拦截了事件,后续的事件将不会再分发给子 View,而是直接交给自身的 onTouchEvent () 方法处理。
  • onTouchEvent():用于处理事件,当事件传递到当前 View 时,会调用该方法来处理事件。如果该方法返回 true,表示当前 View 处理了事件,事件将不会再向上或向下传递;如果返回 false,则表示当前 View 未处理事件,事件将继续向上传递给父 View 的 onTouchEvent () 方法或向下传递给子 View 的 dispatchTouchEvent () 方法。

解决过滑动冲突吗?

在 Android 开发中,经常会遇到滑动冲突的问题,例如在一个 ListView 中嵌套了一个 HorizontalScrollView,或者在一个 ViewGroup 中同时有水平和垂直方向的滑动需求等。解决滑动冲突的方法主要有以下几种:

  • 外部拦截法:在父 View 的 onInterceptTouchEvent () 方法中根据一定的条件来拦截事件,决定是否将事件分发给子 View。例如,当水平方向滑动距离超过一定阈值时,拦截事件并自己处理水平滑动,否则将事件分发给子 View 处理垂直滑动。
  • 内部拦截法:在子 View 的 dispatchTouchEvent () 方法中通过调用 getParent ().requestDisallowInterceptTouchEvent (true) 方法来请求父 View 不要拦截事件,然后在子 View 的 onTouchEvent () 方法中根据一定的条件来决定是否将事件交给父 View 处理。
  • 使用 NestedScrolling 机制:Android 提供了 NestedScrolling 机制来解决滑动冲突问题,通过实现 NestedScrollingParent 和 NestedScrollingChild 接口,在父 View 和子 View 之间进行协调和交互,实现更加灵活的滑动效果。

自定义过 View 吗?如何实现的?

自定义 View 通常需要以下几个步骤:

  • 继承 View 或其子类:根据具体的需求选择合适的父类进行继承,如果只是简单的绘制自定义图形或显示特定的内容,可以继承 View;如果是具有特定布局和子 View 的组合,可以继承 ViewGroup 等。
  • 重写构造方法:通常需要重写 View 的多个构造方法,以确保在不同的情况下都能正确地创建 View。在构造方法中可以进行一些初始化操作,如设置默认属性、加载布局等。
  • 测量 View 的大小:重写 onMeasure () 方法,根据 View 的布局参数和内容来测量 View 的大小。在该方法中需要调用 setMeasuredDimension () 方法来设置 View 的测量宽高。
  • 绘制 View 的内容:重写 onDraw () 方法,在该方法中使用 Canvas 和 Paint 等工具来绘制 View 的内容,如绘制图形、文本、图片等。
  • 处理事件:根据需要重写 View 的事件处理方法,如 onTouchEvent ()、onClickListener () 等,以响应用户的操作和交互。

触发 invalidate () 和 requestLayout () 会发生什么?

  • invalidate():调用该方法会使 View 的显示区域无效,从而触发 View 的重绘。它会将 View 的脏区域标记为需要重绘,然后在下次绘制时,系统会调用 View 的 onDraw () 方法来重新绘制该 View。通常用于当 View 的显示内容发生变化时,如改变了 View 的背景颜色、绘制了新的图形等,需要重新绘制 View 的情况。
  • requestLayout():调用该方法会使 View 的布局重新计算和布局。它会将 View 标记为需要重新布局,然后在下次布局时,系统会调用 View 的 onMeasure () 和 onLayout () 方法来重新测量和布局该 View。通常用于当 View 的大小、位置或布局参数发生变化时,需要重新调整 View 的布局的情况。

当调用 invalidate () 方法时,只会触发 View 的重绘,而不会重新布局;而当调用 requestLayout () 方法时,不仅会触发 View 的重绘,还会触发 View 的重新布局,因为布局的变化可能会导致 View 的显示内容也需要重新绘制。

现在需要设置 RecyclerView 的每个 item 都不一样如何实现?

要实现 RecyclerView 的每个 item 都不一样,可以从以下几个方面入手:

  • 使用不同的 ViewHolder:为不同类型的 item 创建不同的 ViewHolder 类,每个 ViewHolder 对应一种特定的布局和数据绑定方式。在 onCreateViewHolder 方法中,根据 item 的类型来实例化相应的 ViewHolder。
  • 重写 getItemViewType 方法:在 Adapter 中重写 getItemViewType 方法,根据数据的不同特征为每个 item 返回一个唯一的类型标识符。例如,可以根据数据中的某个字段或者数据的来源来判断类型,然后返回不同的整数值作为类型标识。
  • 根据类型加载不同布局:在 onCreateViewHolder 方法中,根据 getItemViewType 返回的类型标识符,使用不同的布局文件来创建 ViewHolder。可以使用 LayoutInflater 的 inflate 方法,传入不同的布局资源 ID 来创建不同的视图。
  • 数据绑定和操作:在 onBindViewHolder 方法中,根据 item 的类型和 ViewHolder 的类型,进行相应的数据绑定和操作。可以通过 ViewHolder 的类型判断来调用不同的方法进行数据设置和事件处理等。

Activity 有哪几种启动模式?Activity 界面跳转解耦的方法有哪些?

  • Activity 的启动模式:
    • standard:这是默认的启动模式,每次启动一个 Activity 都会创建一个新的实例并放入任务栈中,不管这个 Activity 是否已经存在。
    • singleTop:如果要启动的 Activity 在任务栈的栈顶已经存在,那么就不会创建新的实例,而是直接复用栈顶的实例,并且会调用 onNewIntent 方法。
    • singleTask:在整个任务栈中,只要该 Activity 存在,就不会创建新的实例,而是将该 Activity 之上的所有 Activity 都出栈,使该 Activity 成为栈顶元素,然后调用 onNewIntent 方法。
    • singleInstance:该模式会为该 Activity 单独创建一个任务栈,并且在整个系统中只有一个实例存在,无论从哪个任务栈中启动该 Activity,都会在这个单独的任务栈中打开。
  • Activity 界面跳转解耦的方法:
    • 使用 Intent:通过显示 Intent 或隐式 Intent 来启动 Activity,在 Intent 中传递必要的数据和参数,这种方式可以将启动者和被启动者在一定程度上解耦,只要知道目标 Activity 的类名或 Action 等信息即可。
    • 使用接口回调:定义一个接口,在启动 Activity 的类中实现该接口,在被启动的 Activity 中通过接口回调的方式将结果返回给启动者,这样可以减少两个 Activity 之间的直接依赖。
    • 使用事件总线:例如使用 EventBus 等开源库,在一个 Activity 中发送事件,在另一个 Activity 中接收事件并进行相应的处理,从而实现解耦。

Fragment 的生命周期是怎样的?

Fragment 的生命周期与 Activity 密切相关,主要有以下几个阶段:

  • onAttach:当 Fragment 与 Activity 关联时调用,此时可以获取到关联的 Activity 实例。通常在该方法中进行一些初始化操作,比如获取 Activity 传递过来的参数等。
  • ** onCreate**:在 Fragment 创建时调用,用于进行一些初始化工作,如创建视图、初始化数据等。但是此时视图还没有创建,不能进行与视图相关的操作。
  • ** onCreateView**:在该方法中创建并返回 Fragment 的视图,通常通过 LayoutInflater 来加载布局文件并返回。可以在该方法中进行视图的初始化和设置。
  • ** onActivityCreated**:当关联的 Activity 的 onCreate 方法执行完毕后调用,此时 Fragment 的视图已经创建完成,可以进行与视图相关的操作,如获取视图中的控件等。
  • ** onStart**:与 Activity 的 onStart 类似,当 Fragment 可见时调用,此时 Fragment 的视图已经可见,但还不能与用户进行交互。
  • ** onResume**:当 Fragment 处于活动状态并可与用户交互时调用,此时 Fragment 的视图处于前台并可以接收用户的输入和操作。
  • ** onPause**:当 Fragment 失去焦点但仍然可见时调用,通常在该方法中保存一些临时数据,如用户在 EditText 中输入的内容等。
  • ** onStop**:当 Fragment 不可见时调用,此时可以进行一些资源的释放和清理工作,但不建议进行耗时操作。
  • ** onDestroyView**:在 Fragment 的视图被销毁时调用,此时可以释放与视图相关的资源,如取消对视图的监听等。
  • ** onDestroy**:在 Fragment 被销毁时调用,用于释放 Fragment 中持有的其他资源,如数据库连接、网络连接等。
  • ** onDetach**:当 Fragment 与 Activity 解除关联时调用,此时可以进行一些最后的清理工作。

Service 的启动方式有哪些?有什么区别?

  • Service 的启动方式:
    • startService:通过调用 Context 的 startService 方法来启动 Service,这种方式启动的 Service 会在后台一直运行,直到调用 stopSelf 或 stopService 方法停止。
    • bindService:通过调用 Context 的 bindService 方法来启动 Service,这种方式启动的 Service 与调用者绑定,当调用者销毁时,Service 也会随之销毁。
    • startForegroundService:这是 Android 8.0 及以上版本中用于在前台启动 Service 的方式,需要在 Service 中创建一个通知并调用 startForeground 方法将 Service 设置为前台服务,以提高 Service 的优先级,避免被系统回收。
  • 区别:
    • 生命周期:startService 启动的 Service 在调用 startService 方法时会执行 onCreate 方法,然后执行 onStartCommand 方法,多次调用 startService 方法时,onStartCommand 方法会多次调用。而 bindService 启动的 Service 在调用 bindService 方法时会执行 onCreate 方法,然后执行 onBind 方法,只有在第一次绑定的时候会执行这两个方法,之后再次绑定不会重复执行。
    • 与调用者的关系:startService 启动的 Service 与调用者之间没有直接的关联,即使调用者退出,Service 仍然会在后台运行。而 bindService 启动的 Service 与调用者是绑定关系,当调用者销毁时,Service 也会随之销毁。
    • 通信方式:startService 启动的 Service 通常不与调用者进行直接通信,而 bindService 启动的 Service 可以通过 onBind 方法返回一个 IBinder 对象,供调用者与 Service 进行通信。

了解 Binder 吗?

Binder 是 Android 系统中的一种进程间通信(IPC)机制,它在 Android 系统中扮演着非常重要的角色。

  • 原理:Binder 基于 C/S 架构,由 Client、Server、Binder 驱动和 Service Manager 四部分组成。Client 和 Server 通过 Binder 驱动进行通信,Service Manager 负责管理系统中的各种服务,Client 通过向 Service Manager 查询获取 Server 的代理对象,然后通过代理对象与 Server 进行通信。
  • 工作流程:当 Server 向 Service Manager 注册服务时,会将自己的 Binder 对象传递给 Service Manager,Service Manager 会为该服务分配一个唯一的标识符。当 Client 需要使用某个服务时,会向 Service Manager 查询该服务的标识符,然后根据标识符获取到 Server 的代理 Binder 对象,Client 通过代理 Binder 对象向 Server 发送请求,请求会通过 Binder 驱动传递给 Server,Server 处理完请求后将结果通过 Binder 驱动返回给 Client。
  • 优势:Binder 机制具有高效性,因为它采用了内存映射的方式,数据在不同进程之间传递时不需要进行多次拷贝,从而提高了通信效率。同时,Binder 机制还具有安全性,它在通信过程中会对数据进行校验和权限验证,确保数据的安全和完整性。
  • 应用场景:Binder 在 Android 系统中被广泛应用,例如,当一个 Activity 需要调用一个 Service 的方法时,就会通过 Binder 机制进行通信。此外,在不同的应用程序之间进行数据共享和交互时,也会使用 Binder 机制。

项目经历介绍,遇到的问题及解决方法

在 [项目名称] 项目中,主要负责 [具体职责]。例如在实现 [功能名称] 时,遇到了 [具体问题]。当时的情况是 [详细描述问题出现的场景和现象],经过排查发现是由于 [分析问题产生的原因] 导致的。为了解决这个问题,首先尝试了 [方法 1],但是效果并不理想,因为 [说明方法 1 不可行的原因]。然后又尝试了 [方法 2],通过 [具体的解决步骤和技术手段],最终成功解决了问题,使得 [功能名称] 能够正常运行,并且在性能和稳定性上都有了很大的提升。在这个过程中,不仅提升了自己的技术能力,还学会了如何在面对复杂问题时冷静分析和寻找解决方案。

Android 性能优化这块,在项目中有获得什么经验吗?比如大图加载问题怎么解决?Glide 缓存的原理是什么?

在项目中,对于 Android 性能优化有很多经验。在大图加载方面,为了解决图片加载内存溢出和卡顿的问题,首先会对图片进行压缩处理,根据图片的显示尺寸和设备的屏幕密度,在加载前将图片压缩到合适的大小,减少内存占用。同时,采用异步加载的方式,避免在主线程中进行图片加载导致的 UI 卡顿。使用图片加载库如 Glide,它内部会自动进行图片的缓存管理,进一步提升加载效率。

Glide 的缓存原理主要包括内存缓存和磁盘缓存。内存缓存使用的是 LruCache 算法,当图片加载到内存中后,会将其缓存在内存中,当再次需要加载相同图片时,直接从内存缓存中获取,提高加载速度。磁盘缓存则是将图片缓存在本地磁盘上,当内存缓存中不存在需要的图片时,会先从磁盘缓存中查找,如果找到则加载并放入内存缓存中。同时,Glide 还会根据图片的缓存策略,自动管理缓存的生命周期和大小,确保缓存的有效性和合理性。

Android 缓存如何缓存,图片如何缓存,数据如何缓存,缓存机制是什么?

在 Android 中,缓存主要有内存缓存和磁盘缓存两种方式。对于图片缓存,通常使用专门的图片加载库如 Glide、Picasso 等来实现。这些库内部会自动管理图片的缓存,它们会将图片以合适的大小缓存在内存中,同时也会在磁盘上保存一份副本。当需要加载图片时,首先会在内存缓存中查找,如果找到则直接使用,否则会从磁盘缓存中查找并加载到内存中,同时更新内存缓存。

对于数据缓存,可以使用 SharedPreferences、SQLite 数据库、文件存储等方式。SharedPreferences 适合存储一些简单的键值对数据,如用户配置信息等。SQLite 数据库则适用于存储结构化的数据,如用户的聊天记录、订单信息等。文件存储可以将数据以文件的形式存储在本地,如缓存网络请求的 JSON 数据等。

缓存机制的核心是根据数据的使用频率和重要性来合理地管理缓存的存储和读取。一般会采用 LRU(最近最少使用)算法来管理内存缓存,当缓存空间不足时,会自动淘汰最近最少使用的缓存数据。对于磁盘缓存,会根据缓存数据的有效期、大小等因素进行定期清理和更新,以确保缓存的有效性和节省存储空间。

js 和 android 耦合是怎么样的?

在 Android 应用中,JavaScript 和 Android 的耦合主要通过 WebView 来实现。WebView 是一个可以在 Android 应用中展示网页内容的视图组件,它提供了 JavaScript 与 Java 之间的交互接口。通过 WebView 的 addJavascriptInterface 方法,可以将 Java 对象暴露给 JavaScript,使得 JavaScript 可以调用 Java 中的方法,实现功能的扩展和数据的交互。例如,在一个混合应用中,JavaScript 可以调用 Android 的相机、相册等本地功能,获取设备的信息等。

同时,Android 也可以通过 WebView 的 loadUrl 方法加载 JavaScript 代码,或者通过 evaluateJavascript 方法在 JavaScript 环境中执行特定的代码,从而实现对 JavaScript 的调用和控制。这种耦合方式使得在 Android 应用中可以灵活地使用 JavaScript 来实现一些动态的、复杂的业务逻辑,同时又能充分利用 Android 的本地资源和功能,提高应用的开发效率和用户体验。

React Native 有什么了解?知道 React Native 有什么不好的地方吗?

React Native 是 Facebook 推出的一个开源的跨平台移动应用开发框架,它允许开发者使用 JavaScript 和 React 来构建原生应用。它的主要优点是代码复用性高,可以在不同的平台上共享大部分代码,提高开发效率,减少开发成本。同时,它能够提供接近原生应用的性能和用户体验,因为它最终会将 JavaScript 代码转换为原生的组件和视图进行渲染。

然而,React Native 也存在一些不足之处。首先,它的性能虽然接近原生,但在一些复杂的动画和高性能要求的场景下,仍然可能无法达到原生应用的水平。其次,由于它是基于 JavaScript 的,对于一些需要使用原生特定功能的场景,可能需要编写大量的桥接代码,增加了开发的复杂性和维护成本。另外,React Native 的版本更新较快,不同版本之间可能存在兼容性问题,需要开发者不断地进行更新和适配。而且,在遇到问题时,由于其涉及到 JavaScript 和原生代码的混合,调试难度相对较大,需要开发者对两种技术都有深入的了解。

Kotlin 以及对其的看法

Kotlin 是一种基于 Java 虚拟机(JVM)的编程语言,与 Java 兼容并可互操作,由 JetBrains 开发并开源。它具有简洁的语法,减少了样板代码,如声明变量和函数时可以省略很多冗余的关键字和修饰符,让代码更易读和维护。它支持函数式编程,有高阶函数、Lambda 表达式、不可变数据类型等特性,使代码更灵活和高效。它还提供了空安全机制,通过在变量声明时明确指定是否可为空,能有效减少空指针异常。Kotlin 与 Java 的互操作性非常好,可以在 Kotlin 项目中轻松调用 Java 代码,反之亦然,这使得在现有 Java 项目中引入 Kotlin 或从 Java 迁移到 Kotlin 都很方便。此外,Kotlin 在 Android 开发中得到了官方的支持,Android Studio 对 Kotlin 有很好的集成,开发效率更高,能提升 Android 应用的开发体验和质量。总体而言,Kotlin 是一门优秀的编程语言,在 Android 开发领域有着广阔的应用前景,值得开发者学习和使用。

Flutter 有没有用过?在 Flutter 里 streams 是什么?stream 有几种订阅模式,如何调用?Flutter 的绘制流程是怎样的?

Flutter 是谷歌推出的一款开源的移动应用开发框架,可用于快速构建高性能、高保真的移动应用。在 Flutter 中,Stream 是一种用于处理异步事件流的抽象概念,类似于其他编程语言中的观察者模式或响应式编程中的流。它可以在一段时间内产生一系列的数据或事件,多个订阅者可以监听这些事件并做出相应的处理。Stream 有两种订阅模式,分别是单订阅模式和广播模式。单订阅模式下,Stream 只会向一个订阅者发送事件,当有新的订阅者时,需要等待当前订阅者取消订阅后才能接收事件;广播模式下,Stream 会向所有订阅者发送事件,每个订阅者都能独立地接收和处理事件。在 Flutter 中,可以通过创建 StreamController 来创建一个 Stream,然后使用 StreamController 的 stream 属性获取该 Stream,并通过调用 stream 的 listen 方法来订阅该 Stream。在 listen 方法中,可以传入一个回调函数,用于处理接收到的事件。Flutter 的绘制流程主要包括以下几个步骤:首先,当应用启动或需要更新界面时,Flutter 会构建一个 Widget 树,Widget 树描述了界面的布局和样式;然后,Flutter 会根据 Widget 树生成一个 Render 树,Render 树负责实际的布局和绘制;接着,Flutter 会将 Render 树中的每个 RenderObject 转换为一个 Layer,Layer 负责管理绘制的顺序和合成;最后,Flutter 会将所有的 Layer 提交给 GPU 进行绘制,GPU 会根据 Layer 的信息进行渲染并显示在屏幕上。

如何解决内存泄漏?

在 Android 开发中,内存泄漏是指程序在运行过程中,不断地申请内存空间却没有及时释放不再使用的内存,导致可用内存逐渐减少,最终可能导致系统内存不足甚至应用崩溃。解决内存泄漏问题可以从以下几个方面入手:首先,对于资源未关闭导致的泄漏,如文件流、数据库连接、网络连接等,在使用完毕后要及时关闭。例如,在使用 SQLite 数据库时,在操作完成后要调用 close 方法关闭数据库连接。其次,要注意内部类引用导致的泄漏。如果内部类持有外部类的引用,并且内部类的生命周期比外部类长,就可能导致外部类无法被释放。此时可以将内部类改为静态内部类,并通过弱引用的方式持有外部类的引用。再者,对于全局变量和单例模式中的对象引用,要谨慎使用,避免在不需要时仍然持有对其他对象的引用。另外,在使用第三方库时,要注意检查其是否存在内存泄漏问题,并及时更新到最新版本或采取相应的解决措施。同时,还可以使用一些内存分析工具,如 Android Studio 自带的 Memory Profiler,来检测和分析内存泄漏的具体位置和原因,以便有针对性地进行解决。最后,要养成良好的编程习惯,在编写代码时就要考虑到内存管理问题,及时释放不再使用的资源和对象。

oom 是什么,应该怎么优化?

OOM 即 OutOfMemoryError,是指在 Java 或 Android 应用程序运行过程中,内存使用超出了系统分配给该应用的最大内存限制,导致系统无法再为应用分配更多的内存,从而引发的错误。优化 OOM 问题可以从以下几个方面着手:首先,要优化内存使用,避免不必要的内存分配。例如,尽量避免在循环中创建大量临时对象,可以将对象的创建放在循环体外;对于一些大的位图资源,可以根据需要进行压缩和采样,以减少内存占用。其次,要合理管理内存,及时释放不再使用的内存。比如,在 Activity 或 Fragment 的生命周期方法中,及时释放不再需要的资源和对象;对于一些缓存数据,要设置合理的缓存大小和过期时间,避免缓存过多的数据导致内存占用过大。再者,可以通过优化算法和数据结构来减少内存使用。例如,使用更高效的数据结构,如 SparseArray、ArrayMap 等替代传统的 HashMap,以减少内存开销。另外,在加载图片等大资源时,可以采用图片加载库,如 Glide、Picasso 等,它们会自动进行图片的缓存和内存管理,避免图片加载导致的 OOM。还可以通过增加应用的内存限制来缓解 OOM 问题,但这需要根据具体情况谨慎使用,因为增加内存限制可能会导致应用在一些低内存设备上运行不稳定。最后,要使用内存分析工具,如 Android Studio 的 Memory Profiler,对应用的内存使用情况进行分析,找出内存占用较大的地方和可能导致 OOM 的原因,有针对性地进行优化。

怎么定位 ANR?

ANR 即 Application Not Responding,是指在 Android 应用程序中,主线程在一段时间内没有响应输入事件或广播事件,导致系统认为应用无响应而弹出 ANR 对话框。定位 ANR 问题可以采用以下方法:首先,查看系统日志,当发生 ANR 时,系统会在 logcat 中输出详细的 ANR 信息,包括发生 ANR 的进程名、线程名、时间戳以及 ANR 的原因等。通过分析这些日志信息,可以初步确定 ANR 发生的位置和可能的原因。其次,可以使用 Android Studio 的 Profiler 工具,在 Profiler 中可以查看应用的 CPU、内存、网络等使用情况,以及各个线程的运行状态。当发生 ANR 时,可以通过 Profiler 查看主线程在 ANR 发生时的调用栈信息,找出可能导致主线程阻塞的代码。再者,通过代码埋点和调试,在可能导致 ANR 的关键代码处添加日志输出,记录代码的执行时间和状态,以便在 ANR 发生后能够更准确地定位问题。另外,还可以使用一些第三方的性能监控工具,如 Bugly、Sentry 等,它们可以自动收集 ANR 信息并上报到后台,方便开发者及时发现和解决 ANR 问题。最后,对于一些复杂的 ANR 问题,可以通过复现问题并使用调试工具进行断点调试,逐步找出导致 ANR 的根本原因。同时,要注意对 ANR 问题进行分类和总结,以便在后续的开发中能够更好地避免和解决类似的问题。

对滴滴印象如何?

滴滴作为一家在出行领域极具影响力的科技公司,给大众留下了深刻的印象。它极大地改变了人们的出行方式,无论是日常通勤、外出游玩还是紧急出行需求,通过滴滴平台都能便捷地叫到车,涵盖了快车、专车、顺风车等多种出行选择,满足了不同用户群体在费用、舒适度等方面的多样化需求。

从技术角度来看,滴滴要支撑海量的用户并发请求,在地图导航、订单匹配、司机调度等诸多方面想必有着强大且复杂的技术体系。其后台需要精准地分析路况、实时调配车辆资源,确保乘客能快速打到车并且司机能高效接到乘客,这背后涉及到大数据、算法优化以及高可靠的系统架构搭建等众多先进技术的运用。

而且,滴滴在保障出行安全方面也一直在不断努力,比如推出行程分享、紧急求助等功能,让乘客在出行过程中有更强的安全感。同时,滴滴也在积极拓展业务边界,像代驾、货运等领域也有所涉足,展现出很强的创新和拓展能力,总体而言是一家充满活力且在不断进步的科技企业。

部门技术栈是什么?Kotlin 语言为主,有一套自己设计的架构,对此有何看法?

以 Kotlin 语言为主是很顺应技术发展潮流的选择。Kotlin 本身语法简洁,相比 Java 能减少很多样板代码,像变量声明、函数定义等方面更加简洁直观,这可以提升代码的可读性以及开发效率。它所具备的空安全特性更是一大亮点,通过明确变量是否可为空,能在很大程度上避免空指针异常的出现,使得代码的健壮性更强。

而部门有一套自己设计的架构,这有着诸多优势。首先,自主设计的架构可以更贴合业务需求,因为通用的架构可能无法完全适配部门所面临的独特业务场景和问题,自己设计就能针对业务特点进行定制化打造,例如在模块划分、交互逻辑等方面可以做到精准匹配,让不同功能模块之间的协作更加顺畅高效。

从长远发展来看,拥有自己的架构便于后续的扩展和维护,随着业务不断发展变化,能够基于这套架构灵活地添加新功能、优化现有功能,而不用担心受制于外部架构的限制。同时,对于团队内部来说,统一的架构有利于新成员快速熟悉项目,大家基于共同的架构理念和规范开展工作,能提升整个团队的协作效率以及代码质量。

对第一份工作有什么期许?

对于第一份工作,最期望的是能有一个良好的学习成长环境。初入职场,需要不断积累专业知识和技能,希望所在的团队有经验丰富的同事,他们可以在工作中给予指导和帮助,无论是遇到技术难题还是对业务理解不够深入时,都能从他们那里汲取经验,快速提升自己的能力水平。

在工作内容方面,希望能够参与到实际的项目开发中,并且所负责的部分具有一定的挑战性,通过攻克这些挑战,让自己的技术能力得到切实的锻炼,比如参与到从需求分析、架构设计到代码实现以及测试上线的完整项目流程中,这样可以全面地了解软件开发的各个环节,拓宽自己的知识面和视野。

也期待公司有完善的培训机制,定期开展一些技术分享、业务培训等活动,让自己可以接触到行业内的前沿知识和先进技术,跟上技术发展的步伐。同时,希望能有机会与不同部门的同事协作交流,了解整个公司的业务运转模式,培养自己的团队协作能力和沟通能力,为今后的职业发展打下坚实的基础。

聊聊实习中做的东西,提出一些问题,并解答这些问题

在实习期间,主要参与了公司移动端应用的一个功能模块开发,这个模块旨在提升用户的交互体验,具体来说是优化了用户在浏览商品详情页面时的图片展示效果。

当时遇到的一个问题是,在加载高清图片时,页面会出现短暂的卡顿现象。经过分析,发现是因为图片尺寸较大,而原有的图片加载逻辑没有对图片进行合适的处理,直接在主线程进行加载导致的。为了解决这个问题,先是引入了专业的图片加载库,利用其自带的图片压缩和异步加载功能,根据手机屏幕的分辨率等参数对图片进行合理压缩后再通过异步的方式加载到页面上,这样就有效避免了主线程的阻塞,解决了卡顿问题。

还有一个问题是,在不同网络环境下,图片加载的成功率参差不齐。针对这个问题,在图片加载的代码中增加了网络状态判断机制,当处于弱网环境时,降低图片的质量要求或者提示用户当前网络不佳,是否继续加载等,同时设置了合理的超时时间,超过这个时间就取消本次图片加载任务,重新尝试或者提示用户,通过这些措施提高了图片加载在不同网络环境下的稳定性。

聊一个实际场景,activity 的生命周期是如何调用的

比如在一个简单的用户登录场景中,当用户点击应用图标启动应用时,首先会创建启动的 Activity,此时该 Activity 的 onCreate 方法会被调用,在这里可以进行一些初始化操作,像加载布局资源、初始化控件、设置一些默认的数据等。

接着 onStart 方法被触发,意味着 Activity 已经可见,但还不能与用户进行交互,这个阶段系统可能在做一些准备工作让 Activity 能完整地展示在用户面前。

随后 onResume 方法执行,这时 Activity 完全处于前台,用户可以对其进行操作,比如在登录页面输入账号密码等交互行为都可以正常进行了。

如果此时来了一个电话或者用户切换到了其他应用,那么当前的 Activity 会进入 onPause 方法,在这里可以暂停一些不需要在后台继续执行的操作,比如暂停视频播放、停止一些动画效果等,以节省系统资源。

当这个 Activity 完全被遮挡住,变为不可见状态时,onStop 方法会被调用,这时可以释放一些只有在可见状态下才需要占用的资源,比如关闭一些只用于展示的传感器监听等。

如果用户又重新切回这个应用,那么会依次执行 onRestart、onStart、onResume 方法,让 Activity 重新回到可交互的前台状态。

而当用户登录成功,跳转到主页面,或者直接按返回键退出应用时,当前 Activity 会先执行 onPause 方法,接着 onStop 方法,最后 onDestroy 方法被调用,在这里可以进行最后的资源清理工作,比如释放一些全局变量的引用、关闭数据库连接等,完成整个 Activity 的生命周期过程。

Spring 的 bean 加载顺序是怎样的?

在 Spring 框架中,bean 的加载顺序受到多种因素影响且遵循一定规则。首先,Spring 容器启动时,会去扫描配置文件或者带有相关注解标记的类路径,来查找需要被管理的 bean 定义信息。

如果存在基于 XML 配置的 bean,按照配置文件里 bean 元素的定义顺序,Spring 会先解析那些不依赖其他 bean 的,也就是没有构造函数注入或者属性注入依赖其他 bean 的,这类 bean 会优先被加载实例化。对于有依赖关系的 bean,Spring 会根据依赖关系来决定顺序,比如一个 bean 的构造函数中依赖了另一个 bean,那么被依赖的 bean 会先被加载实例化,以保证依赖注入时所依赖的对象已经存在,这体现了一种依赖优先的原则。

在基于注解的配置中,像 @Component、@Service 等注解标注的类,没有依赖关系的情况下,按照类被扫描到的先后顺序来处理,不过通常很难去精准判断这个先后顺序,因为它和类路径的扫描顺序等因素有关。但一旦出现依赖注入情况,同样也是先加载被依赖的 bean。

另外,若配置了一些初始化方法,比如实现了 InitializingBean 接口的 afterPropertiesSet 方法或者通过 @PostConstruct 注解标记的方法,会在 bean 实例化且完成依赖注入后被调用,用于做一些额外的初始化操作,像建立数据库连接、加载配置文件数据等,这也算是 bean 加载过程中的一个环节,整体上 Spring 就是通过这样一套机制来有序地完成 bean 的加载并管理它们在容器中的生命周期。

进程线程的抽象理解是怎样的?

进程可以被抽象地看作是操作系统中正在运行的一个独立程序实例,它拥有自己独立的地址空间、代码段、数据段以及系统资源等,就好像是一个独立的 “工厂”。每个进程都在操作系统分配的这片独立空间里运行,彼此之间相对隔离,例如不同的浏览器进程打开不同的网页窗口时,它们的数据和运行状态不会轻易相互干扰,这保证了程序运行的稳定性和安全性。

而线程则像是这个 “工厂” 里的一条条 “生产线”,它是进程内部的执行单元,共享所在进程的地址空间、代码段、数据段以及所分配到的系统资源等。多个线程可以协同工作来完成进程的任务,比如在一个文字处理软件进程中,可能有一个线程负责接收用户的输入,另一个线程负责实时地进行语法检查,还有线程负责将文档保存到磁盘等,它们共同协作让这个文字处理软件能正常运作。

线程相比于进程,创建和切换的开销更小,因为不需要重新分配一套独立的资源,能更高效地利用进程内的资源来实现并发执行,提升程序的运行效率。不过由于线程共享资源,也可能会带来一些如数据同步等问题,需要通过合适的机制来保证线程安全,而进程间相对独立,通信就需要借助特定的进程间通信机制来实现交互。

操作系统层面的理解有哪些?

从操作系统层面来看,它是整个计算机系统的核心管理者,起到了承上启下的关键作用。向上,它为各类应用程序提供了一个统一且稳定的运行环境,使得不同的软件能够方便地开发、运行和交互,不管是文字处理软件、浏览器还是游戏等,都可以在操作系统搭建的这个平台上顺利工作,而不用去关心底层硬件的复杂细节。

在资源管理方面,操作系统负责对计算机的硬件资源进行合理分配和调度,像 CPU 资源,它会根据不同进程、线程的优先级以及运行状态等因素,通过进程调度算法,比如先来先服务、时间片轮转等,把 CPU 的执行时间分配给各个需要运行的进程,确保系统整体的高效运行。对于内存资源,操作系统会进行内存的分配、回收以及虚拟内存的管理,当有进程申请内存时,它会划分出合适的内存区域给进程使用,当进程结束后回收内存,同时利用虚拟内存技术让物理内存可以运行比实际容量更多的程序。

在文件管理上,操作系统构建了文件系统,定义了文件的存储结构、命名规则以及访问方式等,方便用户和应用程序对文件进行操作,例如创建、删除、读取和写入文件等操作都可以通过操作系统提供的接口来完成。而且操作系统还负责设备管理,对计算机的各种外部设备如打印机、磁盘、键盘等进行驱动和管理,让它们能与计算机主机协调工作,总的来说操作系统把控着整个计算机系统的运行命脉。

进程之间通信是如何实现的?

在操作系统中,进程间通信(IPC)有多种实现方式。

一种常见的方式是管道(Pipe),它分为无名管道和有名管道。无名管道主要用于具有亲缘关系的进程间通信,比如父子进程之间,通过系统调用创建管道后,一个进程往管道写入数据,另一个进程从管道读取数据,数据在管道中按照先进先出的顺序传递,就好像是在两个进程间搭建了一个单向的数据传输通道。有名管道则可以在无亲缘关系的进程间通信,它有一个文件名标识,不同的进程可以通过这个文件名来打开管道进行读写操作。

共享内存(Shared Memory)也是一种高效的通信方式,多个进程可以将同一块物理内存区域映射到各自的虚拟地址空间中,这样这些进程就可以直接对这块共享内存进行读写操作,就如同它们在访问自己内部的内存一样,不过这种方式需要配合一些同步机制,比如信号量等,来避免多个进程同时读写造成的数据混乱问题,因为共享内存本身没有对访问进行限制。

消息队列(Message Queue)则是把消息以队列的形式组织起来,一个进程可以往消息队列中发送消息,另一个进程从消息队列中接收消息,消息按照一定的顺序排队等待被读取,这种方式比较灵活,适用于不同进程间传递不同类型和格式的数据,而且消息队列有自己的标识符,便于不同进程识别和操作。

此外,还有信号(Signal)机制,它类似于一种软中断,一个进程可以向另一个进程发送信号,接收信号的进程在收到信号后会根据预先设定好的信号处理函数来做出相应的反应,比如收到终止信号就执行一些资源清理然后退出运行等,不过信号传递的信息量比较有限,通常用于简单的事件通知等情况。还有套接字(Socket)通信,常用于不同主机上的进程间通信,通过网络协议实现数据的传输,在网络编程中应用广泛,像常见的客户端和服务器之间的通信很多时候就是通过套接字来完成的。

AMS 是如何启动的?AMS 在 Android 起到什么作用?AMS 有哪些应用场景?我们是如何应用 AMS 核心原理的?

AMS 的启动过程:在 Android 系统启动时,首先是 Linux 内核启动并加载一些必要的驱动等基础模块,之后会启动 init 进程,init 进程会解析 init.rc 等初始化脚本文件来启动一系列的系统服务,其中就包括启动 Zygote 进程。Zygote 进程会预加载一些常用的类和资源等,来为后续快速创建应用进程做准备。当需要启动一个新的应用或者系统相关的 Activity 等组件时,Zygote 进程会通过 fork 机制创建出一个新的应用进程,在这个新进程中会启动 ActivityThread,ActivityThread 会去和 ActivityManagerService(AMS)进行交互,从而使得 AMS 开始参与到整个应用组件的管理中,实际上 AMS 在系统启动初期就已经作为重要的系统服务被启动起来了,后续持续发挥着管理组件等作用。

AMS 的作用:AMS 在 Android 系统中起着至关重要的作用,它主要负责管理 Android 系统中的四大组件(Activity、Service、Broadcast Receiver、Content Provider)。例如对于 Activity,它管理着 Activity 的生命周期,决定何时创建、启动、暂停、停止以及销毁一个 Activity,协调不同 Activity 之间的跳转关系,像处理 Activity 的栈管理,确保回退栈等操作符合用户的交互逻辑。对于 Service,它掌控着 Service 的启动、停止以及和其他组件的关联情况等。在广播方面,它负责广播的注册、发送以及接收的调度等工作,保证广播能准确地传达到相应的接收者。对于 Content Provider,也对其数据共享等相关事务进行管理,确保不同应用间能通过 Content Provider 安全、有序地共享数据。

AMS 的应用场景:在日常的应用开发中,每当进行 Activity 的跳转时,就是 AMS 应用场景的体现,它会根据配置和当前的状态来决定如何处理这个跳转,是创建新的 Activity 还是复用已有的等。当启动一个后台 Service 来执行长时间的任务,比如音乐播放服务等,AMS 负责协调这个 Service 的启动以及后续的运行管理。在广播机制里,当系统发送一个电量变化、网络变化等广播时,AMS 会将广播准确地分发到注册了相关接收功能的应用组件那里,实现信息的传递和相应的处理。

应用 AMS 核心原理:开发中可以利用 AMS 对 Activity 生命周期的管理原理来优化应用的性能,比如合理地控制 Activity 的创建和销毁时机,避免不必要的资源占用。在处理多任务切换场景时,依据 AMS 的栈管理逻辑,确保应用的界面切换流畅且符合用户预期,像实现不同 Activity 之间的无缝切换效果等。还可以参考 AMS 对广播分发的原理,设计自己的应用内广播机制,使得信息在不同模块间能高效、准确地传递,提高整个应用的协作性和稳定性,通过深入理解和应用这些核心原理来提升应用开发的质量和用户体验。

WMS 的工作原理说说?

WMS 作为 Android 窗口管理系统的核心,运行在系统进程 system_server 进程中。主要负责以下工作:

  • 窗口的创建和销毁:当应用请求创建窗口时,WMS 会协调各模块完成创建工作,分配内存、图形缓冲区等资源。在销毁窗口时,会回收这些资源。例如,当一个 Activity 启动时,会通过 WMS 创建对应的窗口,当 Activity 结束时,WMS 会销毁该窗口并释放相关资源。
  • 窗口的布局管理:根据应用请求、系统配置等确定窗口在屏幕上的位置和大小。如对于不同屏幕方向和分辨率的设备,WMS 会调整窗口布局,确保应用界面的正常显示。
  • 窗口的显示和隐藏:当用户切换应用或最小化应用时,WMS 会相应地隐藏或重新显示对应的窗口。比如,当用户按下多任务键,WMS 会隐藏当前应用的窗口,并显示最近使用的应用窗口列表。
  • 窗口的动画效果管理:在窗口进行切换、打开或关闭时,WMS 控制动画的执行,使过渡更加自然流畅,如淡入淡出、滑动等动画效果都是由它来调度。
  • 输入事件处理:当用户触摸屏幕或通过硬件按键操作时,WMS 会判断哪个窗口应该接收输入事件,并将事件传递给对应的窗口进行处理。例如,当用户点击屏幕上的某个位置,WMS 会确定该位置所属的窗口,并将点击事件传递给该窗口。

JVM 的核心原理懂多少?JVM 的内存划分是怎样的?

JVM 的核心原理主要包括类加载机制、垃圾回收机制和运行时数据区。

  • 类加载机制:负责将类的字节码文件加载到 JVM 内存中,并转化为 JVM 可以直接使用的类对象。类加载过程包括加载、验证、准备、解析和初始化等阶段,确保类的正确性和安全性。
  • 垃圾回收机制:通过可达性分析等算法判断对象是否可回收,自动管理内存的分配和释放,避免内存泄漏和溢出。常见的垃圾收集算法有标记 - 清除算法、复制算法、标记 - 整理算法等。

JVM 的内存划分主要包括以下几个区域:

  • 程序计数器:是一块较小的内存空间,用于记录当前线程执行的字节码指令的地址,以便线程切换后能恢复到正确的执行位置。
  • 虚拟机栈:为每个线程创建,用于存储线程执行方法时的栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。当方法调用时,栈帧入栈;方法结束时,栈帧出栈。
  • 本地方法栈:与虚拟机栈类似,但是为 Native 方法服务。
  • Java 堆:是 JVM 中最大的一块内存区域,几乎所有的实例对象都在堆中存放,是垃圾收集器管理的主要区域。Java 堆可以细分为新生代和老年代,新生代又可进一步分为 Eden 空间、From Survivor 空间、To Survivor 空间。
  • 方法区:用于存放类信息、常量、静态变量等。方法区是各个线程共享的区域,在 JDK1.7 中,常量池被转移到堆里面。

如何批量发布?

在 Android 中实现批量发布可以采用以下方法:

  • 使用发布工具:如使用 Gradle 构建工具,可以通过配置脚本来实现批量发布。在 build.gradle 文件中,可以定义多个不同的构建变体,如不同的渠道、不同的环境等,然后通过命令行参数或配置切换来一次性构建和发布多个变体。例如,可以配置不同的 productFlavors 来区分不同的应用市场渠道,然后使用 gradle 命令加上相应的参数来同时构建和发布到多个渠道。
  • 脚本自动化:编写脚本语言如 Python、Shell 等来实现批量发布的自动化流程。脚本可以集成构建、签名、上传等操作,通过循环遍历不同的配置和目标环境,依次执行发布操作。比如,可以编写一个 Python 脚本,读取配置文件中的不同应用版本、发布渠道等信息,然后调用相应的构建和发布命令,实现批量发布。
  • 持续集成 / 持续部署(CI/CD)工具:利用如 Jenkins、GitLab CI 等 CI/CD 工具来配置和管理批量发布流程。在这些工具中,可以设置多个构建任务和发布阶段,根据代码提交和触发条件自动执行批量发布。例如,在 Jenkins 中,可以创建多个构建 job,每个 job 对应一个发布目标或变体,然后通过流水线配置将这些 job 串联起来,实现自动化的批量发布。

应用崩溃了怎么办,如何收集崩溃信息?

当应用崩溃时,可以采取以下措施来处理和收集崩溃信息:

  • 使用崩溃捕获框架:如 Crashlytics、Bugly 等第三方崩溃捕获框架,或者在应用中集成自己实现的崩溃捕获机制。这些框架可以在应用崩溃时自动捕获异常信息,包括异常类型、堆栈跟踪、设备信息、应用版本等,并将这些信息上传到服务器端进行分析和统计。
  • 收集日志信息:在应用中合理地使用日志输出,记录关键操作、业务流程和可能出现问题的地方。当应用崩溃后,可以通过查看日志文件来获取更多的上下文信息,帮助定位问题。可以使用 Android 系统提供的 Log 类来输出日志,也可以将日志输出到本地文件中,以便在崩溃后进行查看。
  • 用户反馈收集:在应用中提供方便的用户反馈渠道,如弹出崩溃提示框,引导用户描述崩溃时的操作和现象,并将用户反馈信息与崩溃信息一起收集和分析。还可以在应用中设置专门的反馈页面或联系方式,鼓励用户主动反馈崩溃问题。

应用网络不好如何判断?

判断应用网络不好可以从以下几个方面入手:

  • 网络连接状态监测:使用 Android 系统提供的 ConnectivityManager 类来获取网络连接状态。可以通过监听网络连接变化的广播,如 CONNECTIVITY_ACTION 广播,在广播接收器中判断当前网络是否连接,以及连接的网络类型是移动数据还是 Wi-Fi 等。如果网络连接状态为断开或连接不稳定,可能意味着网络不好。
  • 网络请求超时判断:在进行网络请求时,设置合理的超时时间。当网络请求超过设定的超时时间未得到响应,可以认为网络状况不佳。例如,在使用 HttpURLConnection 或 OkHttp 等网络请求库时,可以通过设置连接超时和读取超时时间来判断网络是否正常。
  • 网络性能指标监测:通过获取网络的性能指标来判断网络质量,如网络延迟、丢包率等。可以使用一些网络性能监测工具或第三方库,如 ping 命令或 Netty 等网络框架提供的性能监测功能。通过定期发送探测数据包并统计响应时间和丢包情况,来评估网络的好坏。
  • 服务器端反馈:在与服务器进行交互时,服务器端可以根据自身的网络状况和业务处理情况向客户端返回相应的状态码或消息。客户端根据服务器端的反馈来判断网络是否存在问题,例如,如果服务器返回 500 内部服务器错误或 503 服务不可用等状态码,可能是网络传输过程中出现了问题。

通信如何保证安全?

在通信过程中保证安全可以通过多方面的措施来实现。

首先,在数据传输方面采用加密技术,例如对传输的信息进行对称加密或者非对称加密。对称加密就是使用相同的密钥对数据进行加密和解密,像 AES 算法,通信双方预先共享好密钥,发送方用该密钥加密数据后传输,接收方再用同样的密钥解密获取原始信息,其优点是加密和解密速度快,适合大量数据传输,但密钥管理要求严格。非对称加密则使用公钥和私钥,如 RSA 算法,发送方用接收方的公钥加密信息,接收方用自己的私钥解密,公钥可以公开,私钥严格保密,安全性高但运算速度相对较慢,常用来传输对称加密的密钥等关键信息。

其次,进行身份验证,确保通信双方的身份真实性。常见的方式有基于证书的认证,比如在 HTTPS 通信中,服务器端会持有由权威机构颁发的数字证书,客户端通过验证证书的合法性、有效期以及证书链等信息来确认服务器的身份,防止遭遇中间人攻击。还有可以采用用户名和密码、指纹识别、面部识别等多种方式结合的多因素认证来增强身份验证的可靠性。

再者,通过数字签名来保证信息的完整性和不可抵赖性。发送方利用自己的私钥对消息摘要进行加密生成数字签名,随消息一起发送给接收方,接收方用发送方的公钥验证签名,若验证通过说明消息未被篡改且确实来自发送方,避免了数据在传输过程中被恶意修改以及发送方事后否认发送行为的情况。

另外,还可以利用安全协议,像 SSL/TLS 协议,它构建在传输层之上,在 HTTP 基础上升级为 HTTPS,为网络通信提供了保密性、数据完整性以及身份验证等多方面的安全保障,对通信过程进行了全方位的安全规范和防护。

计算机网络:socket、http 与操作系统层面的处理

  • Socket:Socket 是在操作系统层面提供的一种网络编程接口,它处于传输层和应用层之间,能够实现不同主机上的进程间通信。在操作系统中,当创建一个 Socket 时,会在内核中分配相应的资源,比如缓冲区等,用于存储发送和接收的数据。Socket 可以基于不同的传输协议创建,像 TCP 或者 UDP。对于 TCP Socket,操作系统会负责建立可靠的连接,通过三次握手确保连接的可靠性,在数据传输过程中会进行流量控制、拥塞控制等,保证数据能准确有序地传输,一旦出现丢包等情况会进行重传;而 UDP Socket 则是无连接的,它不保证数据传输的可靠性,只是简单地将数据报发送出去,速度相对较快,常用于实时性要求较高但对数据完整性要求没那么严格的场景,如视频直播等。
  • HTTP:HTTP 是应用层的协议,用于在万维网上传输超文本等数据。在操作系统层面,当应用程序发起 HTTP 请求时,底层会通过 Socket 等网络接口将请求发送出去,操作系统会将应用层的 HTTP 数据进行层层封装,先加上传输层的 TCP 或 UDP 头部(一般是 TCP),再加上网络层的 IP 头部等,最终通过物理网络发送到目标服务器。服务器接收到请求后,同样按照协议栈进行解包处理,然后处理 HTTP 请求并返回相应的响应。HTTP 本身有多个版本,如 HTTP/1.1、HTTP/2 等,不同版本在性能、功能等方面有所差异,像 HTTP/2 采用了二进制分帧等技术提升了传输效率。
  • 操作系统层面的关联处理:操作系统要协调不同网络协议和应用程序之间的关系,管理网络资源。例如,对于网络连接的管理,操作系统要维护连接状态表,记录各个 Socket 连接的状态;在多任务环境下,要合理分配网络带宽资源给不同的进程和线程使用,当有多个网络请求同时发起时,通过调度算法来决定哪个请求先使用网络资源;同时,操作系统还负责网络设备驱动的管理,使得网络接口卡等硬件设备能正常工作,接收和发送网络信号,为上层的网络通信如 Socket 通信、HTTP 通信等提供坚实的底层基础保障。

进程与线程的区别

进程和线程有着多方面的区别:

  • 资源分配方面:进程是操作系统进行资源分配的基本单位,它拥有独立的地址空间,包括代码段、数据段、堆栈等,不同进程之间的资源是相互隔离的,每个进程都像是一个独立的 “容器”,有自己专属的系统资源分配,例如内存空间、文件句柄等,彼此不会轻易相互干扰。而线程是进程内部的执行单元,它共享所在进程的地址空间以及其他资源,比如多个线程可以访问同一个进程内的全局变量、打开的文件等,就如同在同一个 “大房子” 里不同的 “工作小组” 共同利用里面的设施开展工作。
  • 调度和切换成本:进程的创建和切换开销相对较大,因为创建一个进程需要分配独立的系统资源,涉及到内存空间的分配、初始化各种数据结构等操作,而在切换进程时,要保存当前进程的运行状态,包括寄存器的值、程序计数器等,然后加载下一个要运行的进程的相关状态信息,这个过程比较复杂。线程则相对轻量级,创建和切换时主要是保存和恢复线程的执行上下文,由于共享进程的资源,不需要重新分配大量的资源,所以开销较小,能更高效地实现并发执行,提升程序的运行效率。
  • 并发性和独立性:进程之间相对独立,每个进程都有自己独立的运行逻辑和生命周期,一个进程崩溃一般不会直接影响到其他进程的正常运行,不过进程间通信相对复杂,需要借助特定的进程间通信机制来实现数据交换和交互。线程虽然能方便地实现并发,在一个进程内多个线程可以同时执行不同的任务,但由于共享资源,一个线程出现问题,比如访问共享变量时出现数据不一致等情况,可能会影响到整个进程的稳定性,并且需要通过同步机制如锁等来保证线程安全。
  • 执行效率:在多处理器环境下,多进程可以并行地在不同的处理器核心上运行,各自执行不同的任务;而多线程在一个进程内可以利用多核处理器实现并行,同时由于线程间共享资源便于通信和协作,在某些需要频繁交互协作的场景下,线程能比进程更高效地完成任务,但如果线程间同步控制不好,容易出现性能问题,比如死锁等情况阻碍程序的正常运行。

类加载器分类。具体的执行次序

Java 中的类加载器主要分为以下几类:

  • 引导类加载器(Bootstrap Class Loader):它是最顶层的类加载器,由 C++ 语言实现,负责加载 Java 核心类库,像 java.lang 包下的类等,这些类对于 Java 程序的运行至关重要,是整个 Java 运行环境的基础。它会从系统属性指定的路径(比如 JDK 的安装目录下的相关 lib 文件夹)中加载类,并且它加载的类对于其他类加载器来说是不可见的,处于类加载层次结构的最顶层,其他类加载器都是以它加载的类为基础进行后续的加载工作。
  • 扩展类加载器(Extension Class Loader):它是由 Java 语言实现的类加载器,主要负责加载 Java 的扩展类库,也就是位于 JDK 安装目录下的 jre/lib/ext 目录或者由系统属性指定的其他扩展目录中的类库,它的父加载器是引导类加载器,会在引导类加载器加载完核心类库后,基于其加载的类以及相关的基础,对扩展类库进行加载,为 Java 应用提供更多功能扩展的类支持。
  • 应用程序类加载器(Application Class Loader):也常被称为系统类加载器,同样由 Java 语言实现,主要负责加载用户类路径(Class Path)上的类,也就是开发人员编写的 Java 类以及所依赖的第三方类库等,它的父加载器是扩展类加载器。它会在扩展类加载器完成工作后,将应用程序中定义的各种类加载到内存中,是日常开发中最常接触到的类加载器,会根据类路径的配置和相关规则去查找和加载类。
  • 自定义类加载器(Custom Class Loader):这是开发人员根据自身需求自定义的类加载器,继承自 ClassLoader 类,用于实现一些特殊的类加载场景,比如从特定的网络位置、加密的文件等非标准的数据源加载类,它的父加载器可以是应用程序类加载器或者其他自定义的父类加载器,在需要突破常规类加载机制,按照特定业务要求加载类时发挥作用。

具体的执行次序遵循双亲委派模型,当一个类加载器收到类加载请求时,它首先会把这个请求委派给它的父类加载器去完成,如果父类加载器能够加载这个类,那就由父类加载器完成加载并返回结果;如果父类加载器无法加载,比如找不到对应的类等情况,才会由当前这个类加载器尝试去加载该类。例如,当应用程序类加载器收到加载一个类的请求时,它会先委派给扩展类加载器,扩展类加载器又会先委派给引导类加载器,引导类加载器如果能加载就直接返回了,如果不能加载,再依次由扩展类加载器、应用程序类加载器去尝试加载,要是它们都不行,最后才考虑自定义类加载器来加载这个类,这样的机制保证了类加载的一致性和安全性,避免类的重复加载以及恶意篡改等问题。

接口和类的区别

接口和类在 Java 等面向对象编程语言中有着诸多明显的区别:

  • 定义目的方面:类主要用于对具有共同特征和行为的一组对象进行抽象和封装,它描述了对象的属性和可以执行的操作,体现的是一种 “是什么” 的概念,比如定义一个 “学生” 类,它有学号、姓名等属性以及学习、考试等行为方法,是实实在在对一种实体对象的抽象建模。而接口更多地是定义一组规范、契约或者标准,规定了实现类必须具备的方法和行为,不涉及具体的实现细节,体现的是一种 “能做什么” 的概念,例如定义一个 “可打印” 接口,里面规定了 print 方法,只要实现这个接口的类就要实现 print 方法来达到能打印的要求,不关心具体怎么打印。
  • 成员构成方面:类中可以包含成员变量(包括实例变量和静态变量)、构造方法、普通方法、抽象方法等多种成员。成员变量可以用来存储对象的状态信息,构造方法用于创建对象实例,普通方法实现具体的业务逻辑,抽象方法则可以留给子类去实现。而接口中只能包含常量(默认被 public static final 修饰)和抽象方法(默认被 public abstract 修饰),从 Java 8 开始可以有默认方法和静态方法,默认方法提供了一种在接口中给出方法默认实现的方式,方便接口的扩展,静态方法则可以直接通过接口名调用,不过总体上接口还是侧重于定义行为规范,没有实例变量和普通构造方法这些与具体对象实例相关的成员。
  • 实现和继承方面:一个类只能继承一个父类(单继承原则),通过 extends 关键字来实现继承,子类可以继承父类的属性和方法,并且可以重写父类的方法来实现差异化的行为,这样的继承关系形成了一种层次化的类结构,便于代码的复用和扩展。而一个类可以实现多个接口,使用 implements 关键字,这使得一个类可以同时遵循多个不同的行为规范,增加了代码的灵活性和功能的多样性,例如一个类既可以实现 “可打印” 接口,又能实现 “可序列化” 接口,具备多种不同的能力。
  • 访问修饰符方面:类的访问修饰符可以是 public、default(默认,也就是没有修饰符时的情况)、abstract(抽象类)、final(最终类)等,不同的修饰符决定了类的可见性范围以及是否能被继承等特性。而接口的访问修饰符通常是 public 或者 default,接口默认是抽象的,所有的方法默认也是抽象的(除了 Java 8 新增的默认方法和静态方法),并且接口一般是用来对外公开一组规范,所以常用 public 修饰,让其他类可以方便地实现它,体现出一种公开的、通用的行为约定特点。
  • 设计使用场景方面:类适合构建复杂的对象体系,比如在企业级应用开发中,通过类的继承、组合等关系来构建不同的业务模块,像用户管理模块、订单管理模块等都可以通过类来详细设计实现。接口则常用于定义不同模块之间的交互标准,或者在设计框架时规定外部实现类需要遵循的规则,例如在开发一个插件系统时,通过接口定义插件的功能规范,让不同的开发者可以按照接口要求编写插件实现类,方便插件的集成和替换,提升整个系统的可扩展性和灵活性。

equal 和 hashcode

在 Java 中,equals 和 hashCode 是两个非常重要且紧密相关的方法,它们都用于对象之间的比较和操作,但有着不同的作用和特点。

  • equals 方法:它定义在 Object 类中,用于比较两个对象是否在逻辑上 “相等”。默认情况下,equals 方法比较的是两个对象的引用是否相同,也就是是否指向同一个内存地址。不过,在很多实际的类中,我们通常会根据业务需求重写 equals 方法,来比较对象的内容是否相等。例如,对于一个自定义的 “Person” 类,里面包含姓名和年龄两个属性,我们可能希望当两个 “Person” 对象的姓名和年龄都相同时,就认为这两个对象是相等的,那就需要重写 equals 方法,在方法中去比较这两个属性的值是否一致。重写 equals 方法时需要遵循一些规则,比如自反性(对于任何非空引用 x,x.equals (x) 应该返回 true)、对称性(对于任何非空引用 x 和 y,如果 x.equals (y) 返回 true,那么 y.equals (x) 也应该返回 true)、传递性(对于任何非空引用 x、y 和 z,如果 x.equals (y) 返回 true 且 y.equals (z) 返回 true,那么 x.equals (z) 也应该返回 true)以及一致性(只要对象的状态没有改变,多次调用 equals 方法应该返回相同的结果)等,这样才能保证 equals 方法的正确性和合理性。
  • hashCode 方法:同样定义在 Object 类中,它返回的是一个对象的哈希码值,这个哈希码值主要用于在一些基于哈希的数据结构中,比如 HashSet、HashMap 等,来确定对象在这些结构中的存储位置。一个好的 hashCode 方法应该尽量保证不同的对象产生的哈希码值均匀分布,减少哈希冲突。根据 Java 的规范,如果两个对象通过 equals 方法比较的结果是相等的,那么它们的 hashCode 值必须相同;但是反过来,如果两个对象的 hashCode 值相同,并不一定意味着它们通过 equals 方法比较也是相等的,这就是所谓的哈希冲突情况。所以在重写 equals 方法时,通常也需要重写 hashCode 方法,以保证这两个方法的一致性。例如,在 HashSet 中添加元素时,先会通过 hashCode 值来确定元素可能存储的位置,如果该位置已经有元素了,再通过 equals 方法进一步判断是否是同一个元素,只有当两个方法的判断结果都符合相应条件时,才会决定是否添加元素或者进行其他操作。

手撕冒泡排序

在面试时,很多公司面试时要手撕冒泡排序,根据这来考察面试者基本功。

冒泡排序(Bubble Sort)是一种简单的排序算法。它通过重复遍历待排序的列表,比较相邻元素并交换它们的位置,直到整个列表排序完成。每次遍历都会把当前未排序部分的最大值“冒泡”到数组的最后。

思路:

  1. 从数组的第一个元素开始,逐个比较相邻的元素。
  2. 如果当前元素大于下一个元素,交换它们的位置。
  3. 每一轮遍历后,最大的元素会被“冒泡”到数组的末尾。
  4. 每次遍历的长度逐渐减少,因为已经排序好的元素已经“浮到”数组的末尾。
  5. 直到没有需要交换的元素时,排序完成。

冒泡排序的优化:

  • 如果在某次遍历中没有进行交换,说明数组已经是有序的,可以提前结束排序,避免不必要的遍历。

时间复杂度:

  • 最坏情况下,时间复杂度为 O(n^2),即当数组是逆序时。
  • 最好情况下(当数组已经是有序的),时间复杂度为 O(n),因为只会进行一次遍历且没有发生任何交换。

空间复杂度:

  • O(1),因为该算法是原地排序,不需要额外的空间。

Java 完整代码实现:

public class BubbleSort {
    public static void main(String[] args) {
        // 测试数组
        int[] arr = {5, 2, 9, 1, 5, 6};
        
        // 调用冒泡排序
        bubbleSort(arr);
        
        // 打印排序后的数组
        System.out.println("Sorted array:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
    
    // 冒泡排序方法
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        
        // 外层循环控制排序的轮数
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false;  // 标记是否发生了交换
            
            // 内层循环进行相邻元素比较和交换
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j + 1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            
            // 如果没有发生交换,说明数组已经有序,提前结束
            if (!swapped) {
                break;
            }
        }
    }
}

代码解释:

  1. 主方法 (main):初始化一个整数数组并调用 bubbleSort 方法进行排序。排序后打印数组。
  2. 冒泡排序方法 (bubbleSort):
    1. 外层循环控制排序的轮次,每一轮确保将最大的元素“冒泡”到末尾。
    2. 内层循环执行相邻元素的比较和交换。如果当前元素比下一个元素大,则交换。
    3. swapped 是一个标记,用来检查在某一轮遍历中是否发生了交换。如果某轮没有交换,说明数组已经是有序的,可以提前结束排序。

算法:洗牌不回到原来位置

以下是一种实现洗牌且不会回到原来位置的算法思路,可以使用 Fisher - Yates 洗牌算法进行改进来满足要求,以下是用 Java 语言实现的示例代码:

import java.util.Random;
 
public class ShuffleWithoutOriginalPosition {
    public static void shuffle(int[] cards) {
        Random random = new Random();
        int n = cards.length;
        for (int i = 0; i < n; i++) {
            int randomIndex = i + random.nextInt(n - i);
            // 交换当前位置和随机选择的位置的元素
            int temp = cards[i];
            cards[i] = cards[randomIndex];
            cards[randomIndex] = temp;
            // 额外检查是否回到原来位置,如果是则重新洗牌这一次
            if (randomIndex == i) {
                i--;
                continue;
            }
        }
    }
 
    public static void main(String[] args) {
        int[] cards = {1, 2, 3, 4, 5};
        shuffle(cards);
        for (int num : cards) {
            System.out.print(num + " ");
        }
    }
}

算法原理如下:

首先,我们遍历数组中的每一个位置(从索引 0 开始到最后一个索引)。对于每一个位置 i,我们希望从当前位置 i 到数组末尾(包含末尾)的范围内随机选择一个位置 randomIndex,然后交换这两个位置上的元素,这样逐步地对整个数组进行打乱操作,这就是 Fisher - Yates 洗牌算法的基本思路。但是为了保证不会回到原来位置,在交换完元素后,我们额外增加了一个检查,如果随机选择的位置 randomIndex 恰好就是当前位置 i,那就意味着这次洗牌没有真正改变元素的位置,回到了原始状态,此时我们将索引 i 减 1,也就是重新对这个位置进行洗牌操作,直到交换后的位置与当前位置不同为止,通过这样的方式不断地对数组进行洗牌,最终实现既打乱了元素顺序又不会回到原来位置的效果。例如,对于初始数组 {1, 2, 3, 4, 5},第一次可能选择索引 0 位置和索引 3 位置进行交换,得到 {4, 2, 3, 1, 5},接着处理索引 1 位置,随机选择一个后面的位置交换元素,依次类推,在每一步都确保不会出现交换后又回到原始排列的情况,最终得到一个全新的排列顺序的数组。

一个字符串交换顺序

以下是一种实现交换字符串顺序的简单算法思路,可以使用字符数组来辅助实现,以下是 Java 语言示例代码:

public class StringReverse {
    public static String reverseString(String str) {
        if (str == null) {
            return null;
        }
        char[] charArray = str.toCharArray();
        int left = 0;
        int right = charArray.length - 1;
        while (left < right) {
            // 交换左右两边的字符
            char temp = charArray[left];
            charArray[left] = charArray[right];
            charArray[right] = temp;
            left++;
            right--;
        }
        return new String(charArray);
    }
 
    public static void main(String[] args) {
        String str = "Hello, World!";
        System.out.println(reverseString(str));
    }
}

算法原理是:

首先将输入的字符串转换为字符数组,这样便于对每个字符进行操作。然后定义两个指针,一个指针 left 指向字符数组的开头(索引为 0 的位置),另一个指针 right 指向字符数组的末尾(索引为字符数组长度减 1 的位置)。接着通过一个循环,只要 left 指针小于 right 指针,就交换这两个指针所指向的字符,然后 left 指针向右移动一位,right 指针向左移动一位,不断重复这个过程,直到 left 指针和 right 指针相遇或者交叉,此时整个字符数组中的字符顺序就被交换过来了,最后再将字符数组转换回字符串返回,就实现了字符串顺序的交换。例如,对于字符串 “Hello, World!”,经过上述交换操作后,就会变成 “!dlroW,olleH”。

最近更新:: 2025/10/23 21:22
Contributors: luokaiwen