首页 文章 精选 留言 我的

精选列表

搜索[面试],共4912篇文章
优秀的个人博客,低调大师

前端高频面试题 JavaScript篇

以下问题都来自于互联网前端面经分享,回答为笔者通过查阅资料加上自身理解总结,不保证解答的准确性,有兴趣讨论的同学可以留言或者私信讨论。 1.JS的异步机制? 2.闭包如何实现? 3.原型链、继承? 4.实现订阅者发布者模式? 5.数组的方法有哪些? 1.JS的异步机制? JS使用一个任务队列记录异步任务的回调函数,当异步任务(或者事件被激发,如鼠标点击)完成后,其回调函数会被添加到该任务队列的末尾,JS主线程在将所有的同步任务执行完毕后,会无限循环地去检查任务队列,如果任务队列不为空,则主线程回去执行任务队列中的任务。 关于异步机制的详细解答,可以参考:Javascript异步机制 2.闭包如何实现? 在计算机科学中,闭包是引用了自由变量的函数。在Javascript中,在一个函数中定义一个内部函数,并且内部函数引用了外部函数作用域的变量,然后将这个内部函数作为外部函数的返回值,这样就构成了一个闭包。如下代码: function wrapper() { var milk = '特仑苏' function drink() { console.log('我喝了' + milk) } return drink } var result = wrapper() result() //我喝了特仑苏 关于闭包的详细解答,可以参考:Javascript闭包 3.原型链、继承? 在Javascript中,每当我们定义一个函数,Javascript引擎就会自动为这个函数对象添加一个prototype属性(也被称作原型),每当我们使用这个函数new创建一个对象时,Javascript引擎就会自动为这个对象中添加一个__ proto __ 属性,并让其指向其类的prototype。当我们访问一个对象的属性时,首先回去寻找该对象本身的属性,如果找不到的话回去寻找该对象的 __ proto __ 作用域下的属性,一直到寻找到该属性或者没有__ proto __为止。这种寻找实例属性的方式我们称作原型链。 当我们让子类的prototype指向父类的实例时,便实现了原型链继承。 // 原型链实现继承的关键代码 Son.prototype = new Father() 当然Javascript的继承方式还有 构造函数继承 、 组合继承 、 寄生继承,更详尽的解答,可以参考:对Javascript 类、原型链、继承的理解 4.实现订阅/发布者模式? var publisher = {}; // 定义发布者 publisher.list = []; // 缓存列表 存放订阅者回调函数 // 增加订阅者 publisher.listen = function(fn) { publisher.list.push(fn); // 订阅消息添加到缓存列表 } // 发布消息 publisher.trigger = function(){ for(var i = 0,fn; fn = this.list[i++];) { var that = this fn.apply(null, arguments); } } 详细答案可以参考:Javascript中理解发布--订阅模式 5.数组的方法有哪些? 这个题属于开放题,答案就比较多了,下面我列举一下比较常用的数组方法:遍历方法: 包括 map、foreach、filter let arr = [{ name:"西瓜", type:"水果" },{ name:"芒果", type:"水果" },{ name:"小龙虾", type:"夜宵" }] arr.forEach((item, index) => { console.log(item.name) }) //分别打印 西瓜 芒果 小龙虾 arr.map((item, index) => { return item.name }) //返回数组["西瓜", "芒果", "小龙虾"] arr.filter((item, index) => { if (item.type == "水果") return true; else return false; }) //返回数组[{ name: 西瓜, type : 水果 }, { name: 芒果, type : 水果 }] forEach:用于遍历数组,无返回值; map:遍历数组之后,对每一项返回一个值,并将这些返回值都推入一个数组,最后返回这个新的数组; filter:对数据进行过滤,回调函数返回值为false的项将被过滤掉,最后返回过滤后的数组。操作方法: 包括 concat、push、pop、unshift、shift、splice let listA = ["西瓜", "芒果"] let listB = ["小龙虾"] let listC = listA.concat(listB) // 返回值:[西瓜, 芒果, 小龙虾] listC.push("鸡腿") // 返回值:4 // 数组值:[西瓜, 芒果, 小龙虾, 鸡腿] listC.pop() // 返回值:"鸡腿" // 数组值:[西瓜, 芒果, 小龙虾] listC.unshift("鸡腿") // 返回值:4 // 数组值:[鸡腿, 西瓜, 芒果, 小龙虾] listC.shift() // 返回值:"鸡腿" // 数组值:[西瓜, 芒果, 小龙虾] listC.splice(1, 1, "冰激凌", "奶茶") // 返回值:[芒果] // 数组值:[西瓜, 冰激凌, 奶茶, 小龙虾] concat:拼接数组,将参数数组拼接到调用数组末尾,并返回这个新数组;值得一提的是,这个方法并不会改变调用数组和参数数组,即上例中的listA、listB); push:在数组尾部插入新的元素,返回插入元素之后的数组长度; pop:从数组尾部删除元素,返回删除的元素; unshift:在数组头部插入新的元素,返回插入元素之后的数组长度; shift:从数组头部删除元素,返回删除的元素; splice:删除元素并插入元素,第一个参数为操作位置X,第二个参数为需要从操作位置X删除元素数量,后面的参数为需要从操作位置X插入的元素,返回删除的元素组成的数组。

优秀的个人博客,低调大师

java并发面试常识之copyonwrite

今天在网上看到一个问题,问除了加锁之外有没有其他方法来保证线程安全。楼下很多回答copyonwrite机制。这个问题回答有很多,但是copyonwrite的回答有点误导人。 copyonwrite机制 和单词描述的一样,他的实现就是写时复制, 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。 java中的copyonwrite java中提供了两个利用这个机制实现的线程安全集合。copyonwritearraylist,copyonwritearrayset。看名字就大概猜到他们之间的关系,copyonwritearrayset的底层实现是copyonwritearraylist。我们接下来看看java的实现。 public E get(int index) { return get(getArray(), index); } get的方法就是普通集合的get没有什么特殊的地方,但是成员变量的声明还是有讲究的,是个用volatile声明的数组,这样就保证了读取的那一刻读取的是最新的数据。 private transient volatile Object[] array; 接下来重点就是add方法了。下面的代码可以明显看出是明显需要reentrantlock加锁的,接下来就是复制数据和添加数据的过程,在setArray的过程中,把新的数组赋值给成员变量array(这里是引用的指向,java保证赋值的过程是一个原子操作)。 public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } finally { lock.unlock(); } } 关于迭代,他采取的是获取传递给迭代器的数组值进行迭代,中间就算加入新的值也迭代不到。在构造函数中就直接赋值给final的成员变量。 private final Object[] snapshot; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } 适用场景 copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据也是不及时的,数据量少还好说,数据太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)。

优秀的个人博客,低调大师

java并发面试常识之LinkedBlockingQueue

谈到ArrayBlockingQueue的特色就是循环队列,然后一把锁,2个条件,完成了功能。本来以为LinkedBlockingQueue也是这样的,结果和预期不一样,LinkedBlockingQueue利用了链表的特点,使用了两把锁,两个条件来控制。是一个锁分离的应用,下面就说说,他的实现,以及为什么ArrayBlockingQueue就不适合锁分离。 主要成员变量 private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition(); private final AtomicInteger count = new AtomicInteger(); 除了两个锁,两个条件外,我这里专门列举了计数器。这个计数器很重要,重要到锁分离要依赖他才能正常运行。 锁分离 使用双锁分离就得注意一点,那就是防止线程夯死。生产线程要唤醒生产线程,消费线程也要唤醒生产线程,消费线程唤醒消费线程,消费线程也要唤醒生产线程。 public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); //唤醒标记 int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { while (count.get() == capacity) { //阻塞生产线程 notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) //唤醒生产线程 notFull.signal(); } finally { putLock.unlock(); } //唤醒消费线程 if (c == 0) signalNotEmpty(); } 生产线程唤醒消费线程 基于上面的介绍,我们来看代码,唤醒标记就是为了生产唤醒消费的,因为可能出现消费线程全部都已经等待了,此时生产线程运作,但是消费线程并不能自己唤醒自己,于是就有了signalNotEmpty()的操作。这里的c是getAndIncrement的值,就是获取计数之前的值。c==0的满足条件就有1个元素,在这种情况下才去唤醒消费线程。 生产线程唤醒生产线程 在获取锁后,如果发现容量达到上限,就阻塞了,等待被唤醒,如果可以加入,就执行enqueue方法,是个很简单的链表添加节点的方法。就是在原来last节点后加节点,然后更新last节点。 private void enqueue(Node<E> node) { last = last.next = node; } 在计数器自增后,判断唤醒标记,如果还能继续生产,就去唤醒生产线程。 消费的方案思想和生产类似,这里就不说代码了。 删除 public boolean remove(Object o) { if (o == null) return false; fullyLock(); try { for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } } 删除代码相对比较简单,主要是要获取两把锁,才能进行删除操作就是fullyLock()和fullyUnlock(),删除掉元素后,还要唤醒生产线程。 void unlink(Node<E> p, Node<E> trail) { p.item = null; trail.next = p.next; if (last == p) last = trail; if (count.getAndDecrement() == capacity) notFull.signal(); } ArrayBlockingQueue为何不适合锁分离 这个主要是循环队列的原因,主要是数组和链表不同,链表队列的添加和头部的删除,都是只和一个节点相关,添加只往后加就可以,删除只从头部去掉就好。为了防止head和tail相互影响出现问题,这里就需要原子性的计数器,头部要移除,首先得看计数器是否大于0,每个添加操作,都是先加入队列,然后计数器加1,这样保证了,队列在移除的时候,长度是大于等于计数器的,通过原子性的计数器,双锁才能互不干扰。数组的一个问题就是位置的选择没有办法原子化,因为位置会循环,走到最后一个位置后就返回到第一个位置,这样的操作无法原子化,所以只能是加锁来解决。 适用场景 LinkedBlockingQueue的优点是锁分离,那就很适合生产和消费频率差不多的场景,这样生产和消费互不干涉的执行,能达到不错的效率,尽量不使用remove操作,获取两把锁的效率更低,可以使用size方法(就是计数器直接返回),这个还是比较重要的,有些集合不适合使用size,例如ConcurrentLinkedQueue,正确应该使用isEmpty()。

优秀的个人博客,低调大师

java并发面试常识之ArrayBlockingQueue

ArrayBlockingQueue是常用的线程集合,在线程池中也常常被当做任务队列来使用。使用频率特别高。他是维护的是一个循环队列(基于数组实现),循环结构在数据结构中比较常见,但是在源码实现中还是比较少见的。 线程安全的实现 线程安全队列,基本是离不开锁的。ArrayBlockingQueue使用的是ReentrantLock,配合两种Condition,实现了集合的线程安全操作。这里稍微说一个好习惯,下面是成员变量的声明。 private static final long serialVersionUID = -817911632652898426L; final Object[] items; int takeIndex; int putIndex; int count; final ReentrantLock lock; private final Condition notEmpty; private final Condition notFull; transient Itrs itrs = null; 赋值的操作基本都是在构造函数里做的。这样有个好处,代码执行可控。成员变量的初始化也是会合并在构造方法里执行的,但是在执行顺序上需要好好斟酌,如果写在构造方法里初始化,则没有相关问题。 阻塞队列的常用场所就是生产者消费者。一般都是生产者放入,消费者从头取数据。下面重点说这两个操作。 这两个操作都是依靠锁来保证线程安全的。 生产操作 public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } } put等放入操作,首先是获取锁,如果发现数据满了,就通过notFull的condition,来阻塞线程。这里的条件判定一定是用while而不是if,多线程情况下,可以被唤醒后发现又满了。 private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); } 这个是入队列的操作。首先获取维护的数组。putindex就是放入操作的标志。这个操作会一直加。达到预定的长度后就变成0从头开始计数。这样插入的操作就是一个循环的操作了,count就是用来做计数的,作为能否插入数据的一个标准,插入数据后就通过notEmpty的condition发出一个信号唤醒消费线程。 消费操作 public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } } 消费的方法也是这样。先获取锁,然后进行条件判断,如果没有数据,则阻塞线程。注意点和put一样。 private E dequeue() { final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); notFull.signal(); return x; } 取数据的时候,也依靠takeIndex,这是一个标志,这个数值也会一直增加,表示取的第一个数据的位置。如果这个标志走到最后,然后变成0,从头再来。这样保证取出的数据都是fifo的顺序。删除的时候如果发现迭代中,则会修改迭代器的遍历。然后通过notFull的condition来唤醒生产线程。 移除操作 public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { if (count > 0) { final int putIndex = this.putIndex; int i = takeIndex; do { if (o.equals(items[i])) { removeAt(i); return true; } if (++i == items.length) i = 0; } while (i != putIndex); } return false; } finally { lock.unlock(); } } 对于remove操作就比较麻烦了,首先获取锁之后,把两个标志位本地化,然后找到要删除的元素的位置。调用removeAt,这里删除需要对标志位做改变。 void removeAt(final int removeIndex) { final Object[] items = this.items; if (removeIndex == takeIndex) { items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); } else { final int putIndex = this.putIndex; for (int i = removeIndex;;) { int next = i + 1; if (next == items.length) next = 0; if (next != putIndex) { items[i] = items[next]; i = next; } else { items[i] = null; this.putIndex = i; break; } } count--; if (itrs != null) itrs.removedAt(removeIndex); } notFull.signal(); } 如果删除的元素是位置和takeindex一样。那就可以直接删除,然后让删除标志位向后移动。如果不是,则从删除的位置开始,进行后面向前面的数据覆盖的操作。直到遇到putindex的前一个位置。然后把那个位置的数据设置为null。并且把putindex的位置往前移动一格,正在迭代的时候要删除数据并且唤醒生产线程。

优秀的个人博客,低调大师

面试问题总结 2018/05/16

Redis与Memcache与什么区别? 只说特点,不说谁好谁坏: Redis是单线程的,支持多种数据类型,定制订阅/发布模式,支持Lua脚本,事务,包括持久化存储。 Memcache也是内存存储,不过memcache还可用于缓存其他东西,例如图片、视频等等,Memcache无法持久化。 个人觉得Redis更加强大,效率上两者差别不大,Memcache能做的东西,Redis基本上也能做,所以就选Redis吧。如果需要持久化存储的选用Redis。 MongoDB有什么优缺点? 特点:MongoDB在海量数据下性能优越,文档数据存储结构,可以快速的实现故障转移。 缺点:不支持事务,占用空间大。 如何做MySQL数据库的优化? 硬件优化,提升硬件 MySQL服务器优化,优化my.ini文件,增加调整buffer,cache 表结构优化,恰当的索引 SQL语句优化,专业的人员来做 MySQL的Select语句实现纵表转横表。 参考:https://blog.csdn.net/fysuccess/article/details/40789869 分布式中的生产中与消费者,说说他们的关系。项目中那些是生产者,那些是消费者。 接口与抽象类的区别,什么时候用接口,什么时候用抽象类 参考:https://www.jianshu.com/p/fa651f8d3180 这些技术你再实际生产环境中是如何应用的 惭愧,很多知识没只懂概念,没实践过,以后要学有所用。 说说JDK8的新特性,JDK10呢 JDK8: Lambda 表达式,直接方法引用, 接口默认方法 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。 Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。 Date Time API − 加强对日期与时间的处理。 Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。 Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。 JDK10新特性:https://www.oschina.net/translate/109-new-features-in-jdk-10 常见的设计模式,你经常用那些设计模式 Spring Bean的生命周期 参考:https://blog.csdn.net/lisongjia123/article/details/52091013 AOP了解多少,在项目中是如何使用的 服务器硬件配置如何,数据存储量有多少 你们现有项目中的瓶颈在哪里,能容纳多大的并发量 惭愧,上面的问题表面我的技术在实际应用中并没怎么发挥。 在使用Redis,MongoDB,等遇到过哪些问题,然后是如何解决的。 项目的用户量有多少,并发量多少,数据量有多少 接下来有什么规划 都用过哪些数据库 大数据的场景下,谈谈以空间换取时间的做法。 Spring的事务 JVM性能优化上,常用的JVM参数。关于新生代,老年代的。 先记录这么多吧,水平还是很有限的。主要问题在于我线上实操经验太少。

优秀的个人博客,低调大师

Java 高级面试知识点汇总!

1、常用设计模式 单例模式:懒汉式、饿汉式、双重校验锁、静态加载,内部类加载、枚举类加载。保证一个类仅有一个实例,并提供一个访问它的全局访问点。 代理模式:动态代理和静态代理,什么时候使用动态代理。 适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 装饰者模式:动态给类加功能。 观察者模式:有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 命令模式:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 创建者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 2、基础知识 Java基本类型哪些,所占字节和范围 Set、List、Map的区别和联系 什么时候使用Hashmap 什么时候使用Linkedhashmap、Concurrenthashmap、Weakhashmap 哪些集合类是线程安全的 为什么Set、List、map不实现Cloneable和Serializable接口 Concurrenthashmap的实现,1.7和1.8的实现 Arrays.sort的实现 什么时候使用CopyOnArrayList volatile的使用 synchronied的使用 reentrantlock的实现和Synchronied的区别 CAS的实现原理以及问题 AQS的实现原理 接口和抽象类的区别,什么时候使用 类加载机制的步骤,每一步做了什么,static和final修改的成员变量的加载时机 双亲委派模型 反射机制:反射动态擦除泛型、反射动态调用方法等 动态绑定:父类引用指向子类对象 JVM内存管理机制:有哪些区域,每个区域做了什么 JVM垃圾回收机制:垃圾回收算法 垃圾回收器 垃圾回收策略 jvm参数的设置和jvm调优 什么情况产生年轻代内存溢出、什么情况产生年老代内存溢出 内部类:静态内部类和匿名内部类的使用和区别 Redis和memcached:什么时候选择redis,什么时候选择memcached,内存模型和存储策略是什么样的 MySQL的基本操作 主从数据库一致性维护 mysql的优化策略有哪些 mysql索引的实现 B+树的实现原理 什么情况索引不会命中,会造成全表扫描 java中bio nio aio的区别和联系 为什么bio是阻塞的 nio是非阻塞的 nio是模型是什么样的 Java io的整体架构和使用的设计模式 Reactor模型和Proactor模型 http请求报文结构和内容 http三次握手和四次挥手 rpc相关:如何设计一个rpc框架,从io模型 传输协议 序列化方式综合考虑 Linux命令 统计,排序,前几问题等 StringBuff 和StringBuilder的实现,底层实现是通过byte数据,外加数组的拷贝来实现的 cas操作的使用 内存缓存和数据库的一致性同步实现 微服务的优缺点 线程池的参数问题 ip问题 如何判断ip是否在多个ip段中 判断数组两个中任意两个数之和是否为给定的值 乐观锁和悲观锁的实现 synchronized实现原理 你在项目中遇到的困难和怎么解决的 你在项目中完成的比较出色的亮点 消息队列广播模式和发布/订阅模式的区别 生产者消费者代码实现 死锁代码实现 线程池:参数,每个参数的作用,几种不同线程池的比较,阻塞队列的使用,拒绝策略 Future和ListenableFuture 异步回调相关 算法相关:判断能否从数组中找出两个数字和为给定值,随机生成1~10000不重复并放入数组,求数组的子数组的最大和,二分查找算法的实现及其时间复杂计算 3、其它 算法:常用排序算法,二分查找,链表相关,数组相关,字符串相关,树相关等 常见序列化协议及其优缺点 memcached内存原理,为什么是基于块的存储 搭建一个rpc需要准备什么 如果线上服务器频繁地出现full gc ,如何去排查 如果某一时刻线上机器突然量变得很大,服务扛不住了,怎么解决 LUR算法的实现 LinkedHashMap实现LRU 定义栈的数据结构,请在该类型中实现一个能够找到栈最小元素的min函数 海量数据处理的解决思路 reactor模型的演变 阻塞、非阻塞、同步、异步区别 Collection的子接口 jvm调优相关 zookeeper相关,节点类型,如何实现服务发现和服务注册 nginx负载均衡相关,让你去实现负载均衡,该怎么实现 linux命令,awk、cat、sort、cut、grep、uniq、wc、top等 压力测试相关,怎么分析,单接口压测和多情况下的压测 你觉得你的有点是什么,你的缺点是什么 spring mvc的实现原理 netty底层实现,IO模型,ChannelPipeline的实现和原理 缓存的设计和优化 缓存和数据库一致性同步解决方案 你所在项目的系统架构,谈谈整体实现 消息队列的使用场景 ActiveMQ、RabbitMQ、Kafka的区别 扫描关注我们的微信公众号,干货每天更新。 image

优秀的个人博客,低调大师

蛇皮的Python面试题目

1.小易有一些彩色的砖块。每种颜色由一个大写字母表示。各个颜色砖块看起来都完全一样。现在有一个给定的字符串s,s中每个字符代表小易的某个砖块的颜色。小易想把他所有的砖块排成一行。如果最多存在一对不同颜色的相邻砖块,那么这行砖块就很漂亮的。请你帮助小易计算有多少种方式将他所有砖块排成漂亮的一行。(如果两种方式所对应的砖块颜色序列是相同的,那么认为这两种方式是一样的。) 例如: s = “ABAB”,那么小易有六种排列的结果: “AABB”,”ABAB”,”ABBA”,”BAAB”,”BABA”,”BBAA” 其中只有”AABB”和”BBAA”满足最多只有一对不同颜色的相邻砖块。 . 输入描述: 1.输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s中的每一个字符都为一个大写字母(A到Z)。 输出描述: 2.输出一个整数,表示小易可以有多少种方式。 输入例子:ABAB 输出例子:2 代码: a = raw_input("") new = [] for i in range(len(a)): new.append(a[i]) f len(set(new)) == 2: #set(new) 返回有序不重复 eg:输入ABAB 那么set(new) = ['A','B'] print 2 elif len(set(new)) == 1:#当输入的字符串是 一个 字符时 那么就只有一种方法了 print 1 else: print 0 #当有ABC三种颜色的地板时 那么 无论如何都超过 相邻的不同颜色对数 超过 1 因此排序方式为0 print '这怎一个狗字了的,蛇皮,皮这一下网易你就很快乐吗?'

优秀的个人博客,低调大师

java面试-数据库事务详解

什么是『事务』? 事务就是一组具有原子性的操作,这一组操作要么全都正确执行,要么全都不执行。事务能保证数据库从一种一致性状态转换为另一种一致性状态。 事务的四大特性ACID 1. 原子性 原子性指的是事务是一个不可分割的操作,要么全都正确执行,要么全都不执行。 2. 一致性 事务开始前和事务结束后,数据库的完整性约束没有被破坏。 3. 隔离性 事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。 4. 持久性 事务结束后,事务的结果必须是永久保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。PS:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。 事务的分类 1. 扁平事务 它是实际生产环境中最常用、最简单的事务类型。 事务从BEGIN WORK开始,从COMMIT WORK或ROLLBACK WORK结束。 缺点:发生错误时回滚到事务的起始位置,无法回滚部分操作。而回滚所有的操作开销太大。 2. 带有保存点的扁平事务 这种事务能设置多个保存点,当发生错误时可以回滚到事务中指定的保存点,而不需要将整个事务回滚。 3. 链事务 4. 嵌套事务 5. 分布式事务 数据库并发访问会出现的问题 1. 更新丢失 当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。当数据库没有加任何锁操作的情况下会发生。 2. 脏读 一个事务读到另一个尚未提交的事务中的数据。该数据可能会被回滚从而失效。如果第一个事务拿着失效的数据去处理那就发生错误了。 3. 不可重复读 不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:1. 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。2. 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。 与『脏读』的区别?脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。 事务的隔离级别 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。 1. Read uncommitted 读未提交 在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。 2. Read committed 读提交 在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。 3. Repeatable read 重复读 在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。 4. Serializable 序列化 该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。 使用事务的注意事项 1. 不要在循环中提交事务 2. 不要使用自动提交 3. 不要使用自动回滚 4. 不要使用长事务

优秀的个人博客,低调大师

java面试-彻底搞懂红黑树

红黑树性质 1、每个结点或是红色的,或是黑色的2、根节点是黑色的3、每个叶结点(NIL)是黑色的4、如果一个节点是红色的,则它的两个儿子都是黑色的。5、对于每个结点,从该结点到其叶子结点构成的所有路径上的黑结点个数相同。 和AVL树的比较 AVL树是一棵严格的平衡树,它所有的子树都满足二叉平衡树的定义。因此AVL树高被严格控制在XXX,因此AVL树的查找比较高效。但AVL树插入、删除结点后旋转的次数比红黑树多。 红黑树用非严格的平衡来降低插入删除时旋转的次数。 因此,如果你的业务中查找远远多于插入、删除,那选AVL树;如果查找、插入、删除频率差不多,那么选择红黑树。 插入过程 默认插入的结点为红色。为何?因为红黑树中黑节点至少是红节点的两倍,因此插入节点的父节点为黑色的概率较大,而此时并不需要作任何调整,因此效率较高。 1. 父为黑 插入后无需任何操作。由于黑节点个数至少为红节点的两倍,因此父为黑的情况较多,而这种情况在插入后无需任何调整,这就是红黑树比AVL树插入效率高的原因! 2. 父为红 父为红的情况破坏了红黑树的性质,此时需要根据叔叔的颜色来做不同的处理。 叔叔为红此时很简单,只需交换爸爸、叔叔和爷爷的颜色即可。此时若爷爷节点和太爷爷节点颜色相同,再以爷爷节点为起始节点,进行刚才相同的操作,即:根据爷爷的兄弟颜色做相应的操作。 叔叔为黑此时较为复杂,分如下四种情况:a)爸爸在左、叔叔在右、我在左以爸爸为根节点,进行一次R旋转。b)爸爸在左、叔叔在右、我在右先以我为根节点,进行一次L旋转;再以我为根节点,进行一次R旋转。c)叔叔在左、爸爸在右、我在左先以我为根节点,进行一次R旋转;再以我为根节点,进行一次L旋转。d)叔叔在左、爸爸在右、我在右以爸爸为根节点,进行一次L旋转。 删除过程 二叉搜索树的删除 若删除二叉搜索树的节点A,实际上删除的是二叉搜索树中序遍历的前驱节点,注意:1. 这个被删除节点要么就是一个叶子节点,2. 要么有且仅有一个左孩子然后将孩子顶替它原来的位置,最后将被删的节点值覆盖待删除的那个节点A。 红黑树按照二叉搜索树的方式删除节点,之后再进行相应的旋转操作,使得删除后的树仍然是一棵红黑树。 定义 待删除节点:要删除的那个节点 实际删除节点:待删除节点的中序遍历前驱 红黑树实际删除节点的性质 实际删除节点要么是叶子节点,要么有且仅有一个左孩子; 若为叶子节点,必为红色; 若实际删除节点还有孩子,则该必为左孩子;a)若左孩子为红色,则实际删除节点必为黑色;b)若左孩子为黑色,则实际删除节点红黑均可以。 约定 蓝色箭头:表示判定点 在删除操作开始前,蓝色箭头首先指向实际删除节点。 『实际删除节点』在图中以『父』表示。 旋转过程开始: 1. 父为红色(待删节点为叶子) 直接删除父节点即可: 2. 父为黑 子为红(待删节点为黑、待删节点子节点为红+左孩子) 用子节点覆盖父节点,并保持父节点的颜色: 3. 父为黑 子为黑(待删节点和子节点均为黑) 3.1. 叔叔为红 PS:叔叔为红,则爷爷必为黑! 父在左 叔在右a)子节点覆盖父节点b)进行一次左旋 父在右 叔在左a)子节点覆盖父节点b)进行一次右旋 3.2. 叔叔为黑 PS:叔叔、爸爸都为黑,那爷爷颜色就不确定了! 祖父红 两个侄子黑以下两种情况操作一致:1.子覆盖父(删除)2.交换祖父和叔叔的颜色。 a)父在左 叔在右b)父在右 叔在左同上。 祖父黑 两个侄子黑以下两种情况操作一致:1. 祖父染成子节点的颜色;2. 子节点染成黑色;3. 叔叔染成红色a)父在左 叔在右b)父在右 叔在左 祖父颜色随意 至少有一个红侄a)红侄为左左(叔左、红侄左)1. 红侄进行一次右旋2. 红侄染成黑色3. 交换叔叔和祖父的颜色b)红侄为左右(叔左、红侄右)1. 红侄进行一次右旋+左旋2. 红侄染成父节点颜色;3. 父节点染成黑色c)红侄为右左(叔右、红侄左)1. 红侄进行一次右旋+左旋2. 红侄染成父节点颜色;3. 父节点染成黑色;d)红侄为右右(叔右、红侄右)1. 红侄进行一次左旋2. 叔叔染成父节点颜色;3. 红侄染成黑色;

优秀的个人博客,低调大师

java面试-Java并发编程(三)——volatile

1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行;但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 与 同步。 通信通信是指消息在两条线程之间传递。既然要传递消息,那接收线程 和 发送线程之间必须要有个先后关系,此时就需要用到同步。通信和同步是相辅相成的。 同步同步是指,控制多条线程之间的执行次序。 2. 通信的方式 2.1 通信方式的种类 线程之间的通信一共有两种方式:共享内存 和 消息传递。 共享内存共享内存指的是多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。 这种方式并没有真正地实现消息传递,只是从结果上来看就像是将消息从一条线程传递到了另一条线程。 消息传递顾名思义,消息传递指的是发送线程直接将消息传递给接收线程。由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。 综上所述:对于共享内存的通信方式,需要进行显示的同步,隐式的通信;而对于消息传递的通信方式,需要隐式的同步,显示的通信。 2.2 Java使用的通信方式 Java使用共享内存的方式实现多线程之间的消息传递。因此,程序员需要写额外的代码用于线程之间的同步。 PS:其实共享内存的方式从实现过程来看,跟消息传递一点关系都没有:一条线程将消息存入共享内存,另一条线程从共享内存中读这条消息。但从结果来看,整个过程就好像是一条消息被从线程A传递到了线程B。这种方式之所以能实现消息传递,依托于两点: 必须有一片共享的内存 必须要实现多线程的同步 3. Java多线程的内存模型(简化版) 所有线程都共享一片内存,用于存储共享变量;此外,每条线程都有各自的存储空间,存储各自的局部变量、方法参数、异常对象。 4. volatile是什么? Java采用共享内存的方式实现消息传递,而共享内存需要依托于同步。Java提供了synchronized、volatile关键字实现同步。此外volatile关键字还拥有一些额外的功能。 5. volatile的使用 在成员变量前加上该关键字即可。 public volatile boolean flag; 1 6. volatile的特性 6.1 重排序 重排序是计算机为了提高程序执行效率而对代码的执行顺序进行调整。你以为代码是一行行顺序执行的,但实际并非如此,重排序详解请移步至:Java并发编程的艺术(二)——重排序 若两行指令之间没有依赖关系,那么计算机可以对他们的顺序进行重排序,但若两行之间的某个变量被volatile修饰后,重排序规则会发生变化。 在以下情况下,即使两行代码之间没有依赖关系,也不会发生重排序: volatile读 若volatile读操作的前一行为volatile读/写,则这两行不会发生重排序 volatile读操作和它后一行代码都不会发生重排序 volatile写 volatile写操作和它前一行代码都不会发生重排序; 若volatile写操作的后一行代码为volatile读/写,则这两行不会发生重排序。 6.2 可见性 什么是内存可见性? “内存可见性”指的是一条线程修改完一个共享变量后,另一个线程若访问这个变量将会访问到修改后的值。即:一条线程对共享变量的修改,对其他线程立即可见。 但如果未对共享变量采用同步机制,那么共享变量的修改不会对其他线程立即可见。 为什么会出现内存不可见的情况? 通过上文可知,在Java中每条线程都有各自独立的存储空间,此外还有一个所有线程共享的内存空间。当开启线程时,系统会将共享内存中的所有共享变量拷贝一份到线程专属的存储空间中。接下来该线程在结束前的所有操作都是基于自己的存储空间进行的。因此,若一条线程改变了一个共享变量,仅仅改变的是这条线程专属存储空间中的变量值;此时若其他线程访问这个变量,访问的仍然是先前从共享存储空间读出来的值。然而我们希望一条线程将某个共享变量修改后,其他线程能立即访问到这个最新的值,而不是失效值。这时就需要同步机制来解决这个问题。 如何确保共享变量的可见性? 要确保所有共享变量对所有线程是可见的,就需要给所有共享变量使用同步。在Java中你可以选择将共享变量用同步代码块包裹或用volatile修饰共享变量。 为什么volatile能保证共享变量的内存可见性? volatile修饰了一个成员变量后,这个变量的读写就会比普通变量多一些步骤。 volatile变量写当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。 volatile变量读当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。 通过对volatile变量读写的限制,就能保证线程每次读到的都是最新的值,从而确保了该变量的内存可见性。 volatile变量赠送的附加功能 进行volatile写操作时,不仅会将volatile变量写入共享内存,系统还会将当前线程专属空间中的所有共享变量写入共享内存。进行volatile读操作时,系统也会一次性将共享内存中所有共享变量读入线程专属空间。这就意味着,如果普通变量在volatile写操作之前被修改,那么在volatile读操作之后就能正确读到他们。但是,在volatile写操作之后被修改的普通变量 和 在volatile读操作之前被访问的普通变量 都不具有内存可见性。 6.3 原子性 什么是原子性? 原子性指的是一组操作必须一起完成,中途不能被中断。 volatile能确保long、double读写的原子性 在Java中的所有类型中,有long、double类型比较特殊,他们占据8字节(64比特),其余类型都小于64比特。在32位操作系统中,CPU一次只能读取/写入32位的数据,因此对于64位的long、double变量的读写会进行两步。在多线程中,若一条线程只写入了long型变量的前32位,紧接着另一条线程读取了这个只有“一半”的变量,从而就读到了一个错误的数据。为了避免这种情况,需要在用volatile修饰long、double型变量。 在内存可见性与原子性上,volatile就相当于是同步的setter和getter函数。但并不具有volatile的重排序规则,同步块只确保同步块内部的指令不发生重排序,并不确保同步块以外的指令的重排序。 PS1:Java中的byte竟然是字节,bit才是比特(位)。PS2:char和short-2字节、int和float-4字节、long和double-8字节、byte-1字节 QA:在同步块中调用wait函数是否会破坏原子性?

优秀的个人博客,低调大师

java面试-Java并发编程(五)——中断

什么是中断? 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断; 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。 中断的相关方法 public void interrupt()将调用者线程的中断状态设为true。 public boolean isInterrupted()判断调用者线程的中断状态。 public static boolean interrupted只能通过Thread.interrupted()调用。它会做两步操作: 返回当前线程的中断状态; 将当前线程的中断状态设为false; 暂停、继续、停止线程(已过时) 以下三个方法都是通过线程对象去调用。 suspend()暂停调用者线程,只释放CPU执行权,不释放锁。由于在不释放资源的情况下进入睡眠状态,容易产生死锁。因此已过时! resume()恢复调用者线程,让他处于就绪状态。 stop()调用stop后,并不会保证资源被正确地释放,它会使程序处于不正确的状态下。 PS:stop和interrupt的区别? 中断的使用 要使用中断,首先需要在可能会发生中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理代码。当需要中断线程时,调用该线程对象的interrupt函数即可。 设置中断监听 Thread t1 = new Thread( new Runnable(){ public void run(){ // 若未发生中断,就正常执行任务 while(!Thread.currentThread.isInterrupted()){ // 正常任务代码…… } // 中断的处理代码…… doSomething(); } } ).start(); 1 2 3 4 5 6 7 8 9 10 11 正常的任务代码被封装在while循环中,每次执行完一遍任务代码就检查一下中断状态;一旦发生中断,则跳过while循环,直接执行后面的中断处理代码。 触发中断 t1.interrupt(); 1 上述代码执行后会将t1对象的中断状态设为true,此时t1线程的正常任务代码执行完成后,进入下一次while循环前Thread.currentThread.isInterrupted()的结果为true,此时退出循环,执行循环后面的中断处理代码。 安全地停止线程 stop函数停止线程过于暴力,它会立即停止线程,不给任何资源释放的余地,下面介绍两种安全停止线程的方法。 循环标记变量 自定义一个共享的boolean类型变量,表示当前线程是否需要中断。 中断标识 volatile boolean interrupted = false; 1 任务执行函数 Thread t1 = new Thread( new Runnable(){ public void run(){ while(!interrupted){ // 正常任务代码…… } // 中断处理代码…… // 可以在这里进行资源的释放等操作…… } } ); 1 2 3 4 5 6 7 8 9 中断函数 Thread t2 = new Thread( new Runnable(){ public void run(){ interrupted = true; } } ); 1 2 3 4 5 循环中断状态 中断标识由线程对象提供,无需自己定义。 任务执行函数 Thread t1 = new Thread( new Runnable(){ public void run(){ while(!Thread.currentThread.isInterrupted()){ // 正常任务代码…… } // 中断处理代码…… // 可以在这里进行资源的释放等操作…… } } ); 1 2 3 4 5 6 7 8 9 中断函数 t1.interrupt(); 1 总结 上述两种方法本质一样,都是通过循环查看一个共享标记为来判断线程是否需要中断,他们的区别在于:第一种方法的标识位是我们自己设定的,而第二种方法的标识位是Java提供的。除此之外,他们的实现方法是一样的。 上述两种方法之所以较为安全,是因为一条线程发出终止信号后,接收线程并不会立即停止,而是将本次循环的任务执行完,再跳出循环停止线程。此外,程序员又可以在跳出循环后添加额外的代码进行收尾工作。 处理中断 上文都在介绍如何获取中断状态,那么当我们捕获到中断状态后,究竟如何处理呢? Java类库中提供的一些可能会发生阻塞的方法都会抛InterruptedException异常,如:BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep。 当你在某一条线程中调用这些方法时,这个方法可能会被阻塞很长时间,你可以在别的线程中调用当前线程对象的interrupt方法触发这些函数抛出InterruptedException异常。 当一个函数抛出InterruptedException异常时,表示这个方法阻塞的时间太久了,别人不想等它执行结束了。 当你的捕获到一个InterruptedException异常后,亦可以处理它,或者向上抛出。 抛出时要注意???:当你捕获到InterruptedException异常后,当前线程的中断状态已经被修改为false(表示线程未被中断);此时你若能够处理中断,则不用理会该值;但如果你继续向上抛InterruptedException异常,你需要再次调用interrupt方法,将当前线程的中断状态设为true。 注意:绝对不能“吞掉中断”!即捕获了InterruptedException而不作任何处理。这样违背了中断机制的规则,别人想让你线程中断,然而你自己不处理,也不将中断请求告诉调用者,调用者一直以为没有中断请求。 QA 为什么catch InterruptedException后会自动清除中断状态?

优秀的个人博客,低调大师

2017年Android面试题总结

原文链接:http://mp.weixin.qq.com/s/EeoSKLcXlAlFCoRJFCBx7Q 一、Android整体架构 谈谈你对android系统(体系)架构的理解 Linux操作系统为核心,从下往上,依赖关系。 应用程序层:包括系统应用以及第三方应用。 应用程序框架:提供应用开发所必须的一些API框架,是软件复用的重要手段 库:android运行时(核心包(相当于JDK提供的包),虚拟机(优化过的JVM));C/C++的一些库 Linux核心:提供了电源管理、进程调度、内存管理、网络协议栈、驱动模型等核心系统服务 android中的四大组件以及应用场景 1、Activity:在Android应用中负责与用户交互的组件。 2、Service:常用于为其他组件提供后台服务或者监控其他组件的运行状态。经常用来执行一些耗时操作。 3、BroadcastReceiver:用于监听应用程序中的其他组件。 4、ContentProvider:Android应用程序之间实现实时数据交换。 二、Activity的生命周期 Activity生命周期 生命周期:对象什么时候生,什么时候死,怎么写代码,代码往那里写。 注意: 1、当打开新的Activity,采用透明主题的时候,当前Activity不会回调onStop 2、onCreate和onDestroy配对,onStart和onStop配对(是否可见),onResume和onPause配对(是否在前台,可以与用户交互) 3、打开新的Activity的时候,相关的Log为: Main1Activity: onPause Main2Activity: onCreate Main2Activity: onStart Main2Activity: onResume MainA1ctivity: onStop 异常状态下的生命周期: 资源相关的系统配置发生改变或者资源不足:例如屏幕旋转,当前Activity会销毁,并且在onStop之前回调onSaveInstanceState保存数据,在重新创建Activity的时候在onStart之后回调onRestoreInstanceState。其中Bundle数据会传到onCreate(不一定有数据)和onRestoreInstanceState(一定有数据)。 防止屏幕旋转的时候重建,在清单文件中添加配置: android:configChanges="orientation" 2、Fragment的生命周期 Fragment生命周期 正常启动 Activity: onCreate Fragment: onAttach Fragment: onCreate Fragment: onCreateView Fragment: onActivityCreated Activity: onStart Activity: onResume 正常退出 Activity: onPause Activity: onStop Fragment: onDestroyView Fragment: onDestroy Fragment: onDetach Activity: onDestroy 三、Activity的启动模式 1、standard:每次激活Activity时(startActivity),都创建Activity实例,并放入任务栈; 2、singleTop:如果某个Activity自己激活自己,即任务栈栈顶就是该Activity,则不需要创建,其余情况都要创建Activity实例; 3、singleTask:如果要激活的那个Activity在任务栈中存在该实例,则不需要创建,只需要把此Activity放入栈顶,即把该Activity以上的Activity实例都pop,并调用其onNewIntent; 4、singleInstance:应用1的任务栈中创建了MainActivity实例,如果应用2也要激活MainActivity,则不需要创建,两应用共享该Activity实例。 四、Activity与Fragment之间的传值 1、通过findFragmentByTag或者getActivity获得对方的引用(强转)之后,再相互调用对方的public方法,但是这样做一是引入了“强转”的丑陋代码,另外两个类之间各自持有对方的强引用,耦合较大,容易造成内存泄漏。 2、通过Bundle的方法进行传值,例如以下代码: //Activity中对fragment设置一些参数 fragment.setArguments(bundle); //fragment中通过getArguments获得Activity中的方法 Bundle arguments = getArguments() 3、利用eventbus进行通信,这种方法实时性高,而且Activity与Fragment之间可以完全解耦 //Activity中的代码 EventBus.getDefault().post("消息"); //Fragment中的代码 EventBus.getDefault().register(this); @Subscribe public void test(String text) { tv_test.setText(text); } 五、Service Service分为两种: 1、本地服务,属于同一个应用程序,通过startService来启动或者通过bindService来绑定并且获取代理对象。如果只是想开个服务在后台运行的话,直接startService即可,如果需要相互之间进行传值或者操作的话,就应该通过bindService。 2、远程服务(不同应用程序之间),通过bindService来绑定并且获取代理对象。 对应的生命周期如下: context.startService() ->onCreate()- >onStartCommand()->Service running--调用context.stopService() ->onDestroy() context.bindService()->onCreate()->onBind()->Service running--调用>onUnbind() -> onDestroy() Servic生命周期 注意 Service默认是运行在main线程的,因此Service中如果需要执行耗时操作(大文件的操作,数据库的拷贝,网络请求,文件下载等)的话应该在子线程中完成。 特殊情况是:Service在清单文件中指定了在其他进程中运行。 六、Android中的消息传递机制 为什么要使用Handler? 因为屏幕的刷新频率是60Hz,大概16毫秒会刷新一次,所以为了保证UI的流畅性,耗时操作需要在子线程中处理,子线程不能直接对UI进行更新操作。因此需要Handler在子线程发消息给主线程来更新UI。 这里再深入一点,Android中的UI控件不是线程安全的,因此在多线程并发访问UI的时候会导致UI控件处于不可预期的状态。Google不通过锁的机制来处理这个问题是因为: 1、引入锁会导致UI的操作变得复杂 2、引入锁会导致UI的运行效率降低 因此,Google的工程师最后是通过单线程的模型来操作UI,开发者只需要通过Handler在不同线程之间切花就可以了。 概述一下Android中的消息机制? Android中的消息机制主要是指Handler的运行机制。Handler是进行线程切换的关键,在主线程和子线程之间切换只是一种比较特殊的使用情景而已。其中消息传递机制需要了解的东西有Message、Handler、Looper、Looper里面的MessageQueue对象。 Handler工作原理 如上图所示,我们可以把整个消息机制看作是一条流水线。其中: 1、MessageQueue是传送带,负责Message队列的传送与管理 2、Looper是流水线的发动机,不断地把消息从消息队列里面取出来,交给Handler来处理 3、Message是每一件产品 4、Handler就是工人。但是这么比喻不太恰当,因为发送以及最终处理Message的都是Handler 为什么在子线程中创建Handler会抛异常? Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。 正确的使用方法是: handler = null; new Thread(new Runnable() { private Looper mLooper; @Override public void run() { //必须调用Looper的prepare方法为当前线程创建一个Looper对象,然后启动循环 //prepare方法中实质是给ThreadLocal对象创建了一个Looper对象 //如果当前线程已经创建过Looper对象了,那么会报错 Looper.prepare(); handler = new Handler(); //获取Looper对象 mLooper = Looper.myLooper(); //启动消息循环 Looper.loop(); //在适当的时候退出Looper的消息循环,防止内存泄漏 mLooper.quit(); } }).start(); 主线程中默认是创建了Looper并且启动了消息的循环的,因此不会报错: 应用程序的入口是ActivityThread的main方法,在这个方法里面会创建Looper,并且执行Looper的loop方法来启动消息的循环,使得应用程序一直运行。 子线程中可以通过Handler发送消息给主线程吗? 可以。有时候出于业务需要,主线程可以向子线程发送消息。子线程的Handler必须按照上述方法创建,并且关联Looper。 七、事件传递机制以及自定义View相关 Android的视图树 Android中View的机制主要是Activity的显示,每个Activity都有一个Window(具体在手机中的实现类是PhoneWindow),Window以下有DecorView,DecorView下面有TitleVie以及ContentView,而ContentView就是我们在Activity中通过setContentView指定的。 Android的View树 事件传分发机制 Android事件分发机制 ViewGroup有以下三个与事件分发的方法,而View只有dispatchTouchEvent和onTouchEvent。 @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } 事件总是从上往下进行分发,即先到达Activity,再到达ViewGroup,再到达子View,如果没有任何视图消耗事件的话,事件会顺着路径往回传递。其中: 1、dispatchTouchEvent是事件的分发方法,如果事件能够到达该视图的话,就首先一定会调用,一般我们不会去修改这个方法。 2、onInterceptTouchEvent是事件分发的核心方法,表示ViewGroup是否拦截事件,如果返回true表示拦截,在这之后ViewGroup的onTouchEvent会被调用,事件就不会往下传递。 3、onTouchEvent是最低级的,在事件分发中最后被调用。 4、子View可以通过requestDisallowInterceptTouchEvent方法去请求父元素不要拦截。 注意 1、事件从Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的View(ViewGroup)开始一直往下(子View)传递。子View 可以通过onTouchEvent()对事件进行处理。 2、事件由父View(ViewGroup)传递给子View,ViewGroup 可以通过onInterceptTouchEvent()对事件做拦截,停止其往下传递。 3、如果事件从上往下传递过程中一直没有被停止,且最底层子View 没有消费事件,事件会反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity 的onTouchEvent()函数。 4、如果View 没有对ACTION_DOWN 进行消费,之后的其他事件不会传递过来。 5、OnTouchListener 优先于onTouchEvent()对事件进行消费。 自定义View的分类 1、对现有的View的子类进行扩展,例如复写onDraw方法、扩展新功能等。 2、自定义组合控件,把常用一些控件组合起来以方便使用。 3、直接继承View实现View的完全定制,需要完成View的测量以及绘制。 4、自定义ViewGroup,需要复写onLayout完成子View位置的确定等工作。 View的测量-onMeasure View的测量最终是在onMeasure方法中通过setMeasuredDimension把代表宽高两个MeasureSpec设置给View,因此需要掌握MeasureSpec。MeasureSpec包括大小信息以及模式信息。 MeasureSpec的三种模式: 1、EXACTLY模式:精确模式,对应于用户指定为match_parent或者具体大小的时候(实际上指定为match_parent实质上是指定大小为父容器的大小) 2、AT_MOST模式:对应于用户指定为wrap_content,此时控件尺寸只要不超过父控件允许的最大尺寸即可。 3、UNSPECIFIED模式:不指定大小的测量模式,这种模式比较少用 下面给出模板代码: public class MeasureUtils { /** * 用于View的测量 * * @param measureSpec * @param defaultSize * @return */ public static int measureView(int measureSpec, int defaultSize) { int measureSize; //获取用户指定的大小以及模式 int mode = View.MeasureSpec.getMode(measureSpec); int size = View.MeasureSpec.getSize(measureSpec); //根据模式去返回大小 if (mode == View.MeasureSpec.EXACTLY) { //精确模式(指定大小以及match_parent)直接返回指定的大小 measureSize = size; } else { //UNSPECIFIED模式、AT_MOST模式(wrap_content)的话需要提供默认的大小 measureSize = defaultSize; if (mode == View.MeasureSpec.AT_MOST) { //AT_MOST(wrap_content)模式下,需要取测量值与默认值的最小值 measureSize = Math.min(measureSize, defaultSize); } } return measureSize; } } 最后,复写onMeasure方法,把super方法去掉: @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureUtils.measureView(widthMeasureSpec, 200), MeasureUtils.measureView(heightMeasureSpec, 200) ); } View的绘制-onDraw View绘制,需要掌握Android中View的坐标体系: View绘制 View的坐标体系是以左上角为坐标原点,向右为X轴正方向,向下为Y轴正方向。 View绘制,主要是通过Android的2D绘图机制来完成,时机是onDraw方法中,其中包括画布Canvas,画笔Paint。下面给出示例代码。相关API不是介绍的重点,重点是Canvas的save和restore方法,通过save以后可以对画布进行一些放大缩小旋转倾斜等操作,这两个方法一般配套使用,其中save的调用次数可以多于restore。 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Bitmap bitmap = ImageUtils.drawable2Bitmap(mDrawable); canvas.drawBitmap(bitmap, getLeft(), getTop(), mPaint); canvas.save(); //注意,这里的旋转是指画布的旋转 canvas.rotate(90); mPaint.setColor(Color.parseColor("#FF4081")); mPaint.setTextSize(30); canvas.drawText("测试", 100, -100, mPaint); canvas.restore(); } View的位置-onLayout 与布局位置相关的是onLayout方法的复写,一般我们自定义View的时候,只需要完成测量,绘制即可。如果是自定义ViewGroup的话,需要做的就是在onLayout中测量自身以及控制子控件的布局位置,onLayout是自定义ViewGroup必须实现的方法。 八、性能优化 布局优化 1、使用include标签,通过layout属性复用相同的布局。 <include android:id="@+id/v_test" layout="@layout/include_view" /> 2、使用merge标签,去除同类的视图 3、使用ViewStub来进行布局的延迟加载一些不是马上就用到的布局。例如列表页中,列表在没有拿到数据之前不加载,这样做可以使UI变得流畅。 <ViewStub android:id="@+id/v_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/view_stub" /> //需要手动调用inflate方法,布局才会显示出来。 stub.inflate(); //其中setVisibility在底层也是会调用inflate方法 //stub.setVisibility(View.VISIBLE); //之后,如果要使用ViewStub标签里面的View,只需要按照平常来即可。 TextView tv_1 = (TextView) findViewById(R.id.tv_1); 4、尽量多使用RelativeLayout,因为这样可以大大减少视图的层级。 内存优化 APP设计以及代码编写阶段都应该考虑内存优化: 1、珍惜Service,尽量使得Service在使用的时候才处于运行状态。尽量使用IntentService IntentService在内部其实是通过线程以及Handler实现的,当有新的Intent到来的时候,会创建线程并且处理这个Intent,处理完毕以后就自动销毁自身。因此使用IntentService能够节省系统资源。 2、内存紧张的时候释放资源(例如UI隐藏的时候释放资源等)。复写Activity的回调方法。 @Override public void onLowMemory() { super.onLowMemory(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); switch (level) { case TRIM_MEMORY_COMPLETE: //... break; case 其他: } } 3、通过Manifest中对Application配置更大的内存,但是一般不推荐 android:largeHeap="true" 4、避免Bitmap的浪费,应该尽量去适配屏幕设备。尽量使用成熟的图片加载框架,Picasso,Fresco,Glide等。 5、使用优化的容器,SparseArray等 6、其他建议:尽量少用枚举变量,尽量少用抽象,尽量少增加类,避免使用依赖注入框架,谨慎使用library,使用代码混淆,时当场合考虑使用多进程等。 7、避免内存泄漏(本来应该被回收的对象没有被回收)。一旦APP的内存短时间内快速增长或者GC非常频繁的时候,就应该考虑是否是内存泄漏导致的。 分析方法 使用Android Studio提供的Android Monitors中Memory工具查看内存的使用以及没使用的情况。 使用DDMS提供的Heap工具查看内存使用情况,也可以手动触发GC。 使用性能分析的依赖库,例如Square的LeakCanary,这个库会在内存泄漏的前后通过Notification通知你。 什么情况会导致内存泄漏 1、资源释放问题:程序代码的问题,长期保持某些资源,如Context、Cursor、IO 流的引用,资源得不到释放造成内存泄露。 2、对象内存过大问题:保存了多个耗用内存过大的对象(如Bitmap、XML 文件),造成内存超出限制。 3、static 关键字的使用问题:static 是Java 中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static 修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(Context 的情况最多),这时就要谨慎对待了。 解决方案 应该尽量避免static 成员变量引用资源耗费过多的实例,比如Context。 Context 尽量使用ApplicationContext,因为Application 的Context 的生命周期比较长,引用它不会出现内存泄露的问题。 使用WeakReference 代替强引用。比如可以使用WeakReference<Context> mContextRef 4、线程导致内存溢出:线程产生内存泄露的主要原因在于线程生命周期的不可控。例如Activity中的Thread在run了,但是Activity由于某种原因重新创建了,但是Thread仍然会运行,因为run方法不结束的话Thread是不会销毁的。 解决方案 将线程的内部类,改为静态内部类(因为非静态内部类拥有外部类对象的强引用,而静态类则不拥有)。 在线程内部采用弱引用保存Context 引用。 查看内存泄漏的方法、工具 1、android官方提供的工具:Memory Monitor(当APP占用的内存在短时间内快速增长或者GC变得频繁的时候)、DDMS提供的Heap工具(手动触发GC) 2、Square提供的内存泄漏检测工具,LeakCanary(能够自动完成内存追踪、检测、输出结果),进行演示,并且适当的解说。 性能优化 1、防止过度绘制,通过打开手机的“显示过度绘制区域”即可查看过度绘制的情况。 2、最小化渲染时间,使用视图树查看节点,对节点进行性能分析。 3、通过TraceView进行数据的采集以及分析。在有大概定位的时候,使用Android官方提供的Debug类进行采集。最后通过DDMS即可打开这个.trace文件,分析函数的调用情况(包括在指定情况下执行时间,调用次数) //开启数据采集 Debug.startMethodTracing("test.trace"); //关闭 Debug.stopMethodTracing(); OOM 避免OOM的一些常见方法: 1、App资源中尽量少用大图。使用Bitmap的时候要注意等比例缩小图片,并且注意Bitmap的回收。 BitmapFactory.Options options = new BitmapFactory.Option(); options.inSampleSize = 2; //Options 只保存图片尺寸大小,不保存图片到内存 BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 2; Bitmap bmp = null; bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts); //回收 bmp.recycle(); 2、结合组件的生命周期,释放资源 3、IO流,数据库查询的游标等应该在使用完之后及时关闭。 4、ListView中应该使用ViewHolder模式缓存ConverView 5、页面切换的时候尽量去传递(复用)一些对象 ANR 不同的组件发生ANR 的时间不一样,主线程(Activity、Service)是5 秒,BroadCastReceiver 是10 秒。 ANR一般有三种类型: 1、KeyDispatchTimeout(5 seconds) 主要类型按键或触摸事件在特定时间内无响应 2、BroadcastTimeout(10 seconds) BroadcastReceiver在特定时间内无法处理完成 3、ServiceTimeout(20 seconds) 小概率类型Service在特定的时间内无法处理完成 解决方案: UI线程只进行UI相关的操作。所有耗时操作,比如访问网络,Socket 通信,查询大量SQL 语句,复杂逻辑计算等都放在子线程中去,然后通过handler.sendMessage、runonUITread、AsyncTask 等方式更新UI。 无论如何都要确保用户界面操作的流畅度。如果耗时操作需要让用户等待,那么可以在界面上显示进度条。 BroadCastReceiver要进行复杂操作的的时候,可以在onReceive()方法中启动一个Service来处理。 九、九切图(.9图)、SVG图片 九切图 点九图,是Android开发中用到的一种特殊格式的图片,文件名以”.9.png“结尾。这种图片能告诉程序,图像哪一部分可以被拉升,哪一部分不能被拉升需要保持原有比列。运用点九图可以保证图片在不模糊变形的前提下做到自适应。点九图常用于对话框背景图片中。 九切图实例 1、1、2部分规定了图像的可拉伸部分,当实际程序中设定了对话框的宽高时,1、2部分就会被拉伸成所需要的高和宽,呈现出于设计稿一样的视觉效果。 2、而3、4部分规定了图像的内容区域。内容区域规定了可编辑区域,例如文字需要被包裹在其内。 微信图片_20180110100007.jpg android5.0的SCG矢量动画机制 1、图像在方法缩小的时候图片质量不会有损失 2、使用XML来定义图形 3、适配不同分辨率 十、Android中数据常见存储方式 1、文件(包括XML、SharePreference等) 2、数据库 3、Content Provider 4、保存在网络 十一、进程间通信 操作系统进程间通信的方法,android中有哪些? 操作系统: 1、Windows:剪贴板、管道、邮槽等 2、Linux:命名管道、共享内存、信号量 Android中的进程通信方式并不是完全继承于Linux: 1、Bundle 2、文件共享 3、AIDL 4、Messenger 5、Content Provider 6、Socket 常见的网络框架 1、HttpURLConnection:在Android 2.2版本之前,HttpClient拥有较少的bug,因此使用它是最好的选择。而在Android 2.3版本及以后,HttpURLConnection则是最佳的选择。它的API简单,体积较小,因而非常适用于Android项目。压缩和缓存机制可以有效地减少网络访问的流量,在提升速度和省电方面也起到了较大的作用。对于新的应用程序应该更加偏向于使用HttpURLConnection,因为在以后的工作当中我们也会将更多的时间放在优化HttpURLConnection上面。特点:比较轻便,灵活,易于扩展,在3.0后以及4.0中都进行了改善,如对HTTPS的支持,在4.0中,还增加了对缓存的支持。 2、HttpClient:高效稳定,但是维护成本高昂,故android 开发团队不愿意在维护该库而是转投更为轻便的 3、okHttp:okhttp 是一个 Java 的 HTTP+SPDY 客户端开发包,同时也支持 Android。需要Android 2.3以上。特点:OKHttp是Android版Http客户端。非常高效,支持SPDY、连接池、GZIP和 HTTP 缓存。默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求。从Android4.4开始HttpURLConnection的底层实现采用的是okHttp。 4、volley:早期使用HttpClient,后来使用HttpURLConnection,是谷歌2013年推出的网络请求框架,非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。 5、xutils:缓存网络请求数据 6、Retrofit:和Volley框架的请求方式很相似,底层网络请求采用okhttp(效率高,android4.4底层采用okhttp),采用注解方式来指定请求方式和url地址,减少了代码量。 7、AsyncTask 十三、常用的图片加载框架以及特点、源码 1、Picasso:PicassoSquare的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现。 2、Glide:模仿了Picasso的API,而且在他的基础上加了很多的扩展(比如gif等支持),支持图片流,因此在做爱拍之类的视频应用用得比较多一些。 3、Fresco:Fresco中设计有一个叫做image pipeline的模块。它负责从网络,从本地文件系统,本地资源加载图片。 为了最大限度节省空间和CPU时间,它含有3级缓存设计(2级内存,1级文件)。Fresco中设计有一个叫做Drawees模块, 方便地显示loading图,当图片不再显示在屏幕上时,及时地释放内存和空间占用。 Fresco是把图片缓存放在了Ashmem(系统匿名内存共享区) 1、Heap-堆内存:Android中每个App的 Java堆内存大小都是被严格的限制的。每个对象都是使用Java的new在堆内存实例化,这是内存中相对安全的一块区域。内存有垃圾回收机制,所以当 App不在使用内存的时候,系统就会自动把这块内存回收。不幸的是,内存进行垃圾回收的过程正是问题所在。当内存进行垃圾回收时,内存不仅仅进行了垃圾回收,还把 Android 应用完全终止了。这也是用户在使用 App 时最常见的卡顿或短暂假死的原因之一。 2、Ashmem:Android 在操作 Ashmem 堆时,会把该堆中存有数据的内存区域从 Ashmem 堆中抽取出来,而不是把它释放掉,这是一种弱内存释放模式;被抽取出来的这部分内存只有当系统真正需要更多的内存时(系统内存不够用)才会被释放。当 Android 把被抽取出来的这部分内存放回 Ashmem 堆,只要被抽取的内存空间没有被释放,之前的数据就会恢复到相应的位置。 不管发生什么,垃圾回收器都不会自动回收这些 Bitmap。当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果一个被抽取的图片需要再绘制一次,系统仅仅需要把它再解码一次,这个操作非常迅速。 十四、在Android开发里用什么做线程间的通讯工具? 传统点的方法就是往同步代码块里些数据,然后使用回调让另外一条线程去读。在Android里我一般会创建Looper线程,然后Hanlder传递消息。 十五、15、Android新特性相关 1、5.0:Material Design、多种设备的支持、支持64位ART虚拟机、Project Volta电池续航改进计划等 2、6.0:动态权限管理、过度动画、支付、指纹等 3、7.0:分屏、通知消息快捷回复、夜间模式、流量保护模式等 十六、网络请求优化 网络请求优化 1、能够缓存起来的尽量去缓存起来,减轻服务器的压力。例如APP中首页的一些数据,又例如首页的图标、文案都是缓存起来的,而且这些数据通过网络来指定可以使app具有更大的灵活性。 2、不用域名,用 IP 直连,省去了DNS域名解析。 3、连接复用、请求合并、请求数据Body可以利用压缩算法Gzip来进行压缩,使用JSON 代替 XML 网络请求的安全性 这块了解的不多。我给你说说我的思路吧,利用哈希算法,比如MD5,服务器给我们的数据可以通过时间戳和其他参数做个加密,得到一个key,在客户端取出数据后根据数据和时间戳再去生成key与服务端给的做个对比。 十七、新技术相关 RXJava:一个异步请求库,核心就是异步。利用的是一种扩展的观察模式,被观察者发生某种变化的时候,可以通过事件(onNext、onError、onComplete)等方式通过观察者。RXJava同时支持线程的调度和切换,用户可以指定订阅发生的线程以及观察者触发的线程。 Retrofit:通过注解的方式来指定URL、请求方法,实质上底层是通过OKHttp来实现的。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。