题目
给定一个非负整数 _numRows
,_生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
最优化问题,考虑用动态规划,看是否符合动态规划的条件。
一般从最终状态做倒推,拆解当前状态问题为上一个状态+有限的步骤。
爬到第n阶,最后一步一定是只有两种可能:
把两种可能的方法数累加就是爬到第n阶第方法数。
爬到第1阶,只有1种爬法,即爬1个台阶。
爬到第2阶,可以从第0阶爬2个台阶,也可以从第1阶爬1个台阶,共2种爬法。
1 | class Solution { |
Java8的lambda和匿名类可以实现类似闭包的功能:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。
但有一个限制:它们不能修改定义Lambda方法的局部变量的内容。这些变量必须是隐式final的。
之所以会有这些限制,主要是由于局部变量是保存在栈上的,如果Lambda可以直接访问局部变量,且Lambda是在另一个线程使用的,那么使用Lambda的线程可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上访问的是它的副本,而如果局部变量仅会赋值一次那就没什么区别了。
如果允许捕获可变的局部变量,就会引发造成线程不安全的可能性,而这是我们极力希望避免的。基于种种考虑,Java直接在语法层面做出限制,这其实也是不鼓励程序员使用改变外部变量的典型的命令式编程模式。
对lambda表达式里的代码,生成静态私有方法,创建一个函数式接口类的实例来调用这个私有静态方法。
这种方式实现的好处在于:
Java8的设计者之一Brian Goetz对于lambda表达式的设计文档中所提到的方案选择:
用invokedynamic指令干了什么?
invokedynamic调用JDK里的LambdaMetafactory类的方法来把Lambda表达式翻译为字节码。
不用invokedynamic的坏处?
如果把Lambda表达式的翻译方式写死在类的字节码,以后不方便统一修改翻译策略了。
Lambda表达式在语法上看起来就跟匿名内部类一样。
但是对于匿名内部类:
Android系统上,Java-Bytecode(JVM字节码)是不能直接运行在Android系统上的,需要转换成Android-Bytecode(Dalvik/ART 字节码)。
为什么呢?比如Java 8的Lambda表达式是通过invokedynamic指令实现的,Android的dex编译器不支持invokedynamic指令,导致Android不能直接支持Java 8。
既然不能直接支持,那就只能在Java-Bytecode转换到Android-Bytecode这一过程中想办法,间接支持。这个间接支持的过程我们统称为Desugar(脱糖)过程。
具体是怎么实现的呢?比如Java 8的Lambda表达式,javac编译过后,Lambda表达式里的逻辑会被编译为私有静态方法,然后用invokedynamic去在运行时动态创建一个类实例,每个Lambda表达式都会对应一个函数式类接口的,在类方法里调用静态方法。脱糖的过程,就是把invokedynamic指令去除,直接调用静态方法。
脱糖即在编译阶段将在语法层面一些底层字节码不支持的特性转换为基础的字节码结构,(比如 List 上的泛型脱糖后在字节码层面实际为 Object); Android 工具链对 Java8 语法特性脱糖的过程可谓丰富多彩,当然他们的最终目的是一致的:使新的语法可以在所有的设备上运行。
D8编译器 已经支持脱糖,让 Java 8 提供的特性(如 lambdas)可以转换成 Java 7 特性。
D8 编译器作为默认的 DEX 字节码文件编译器,具有更好的性能;
R8 作为 ProGuard 的替代工具,用于代码的压缩(shrinking)和混淆(obfuscation);
Java 7-8-9 等等新引入的语言特性并不能直接就能用在 Android 开发中,基本上现在的所有的 Android 开发者还在被困在 Java 6 SE 上。
为了让我们能使用上 Java 8 的特性,Google 使用了 Transformation 来增加了一步编译过程叫 desugaring,其实就是将我们代码里使用的 java 8 新特性翻译为 Dalvik/ART 能够识别的 java 6 字节码。这不可避免会导致一个问题 - 更长的编译时间。
为了解决这个问题,在 Android Studio 3.2 中,Google 使用 D8 替换了旧的 dx 编译器。D8 的主要改进是消除 desuguaring 的过程,让其成为 dex 编译的一部分,从而加快编译速度。
ASM 是一个 Java 字节码操控框架。
ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。
ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
ASM的应用场景有AOP(CGLIB就是基于ASM)、热部署、修改其他jar包中的类等。
常见的字节码操作工具有:
ASM优势:
ASM劣势:
JavaAssist优势:
JavaAssist劣势:
所以ASM适用于对性能和体积敏感的场景。
参考:
把可变的和不变的分离。
具体而言,被访问者是不变的,而访问者是可变的。举个例子来说,我是不变的,而不同的人看我会有不同的眼光,这个看我的眼光是可变的。
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。
访问者模式适用于数据结构相对稳定,算法又易变化的系统。
因为访问者模式使得算法操作增加变得容易。
若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。
访问者模式的优点:
增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。
访问者模式的缺点:
增加新的数据结构很困难。
简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。
参考:
.class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。
ClassReader 的 accept 方法中传进来了一个参数ClassVisitor。在内部,ClassVisitor会不断的读取ClassReader的二进制byte[],然后在解析后通过参数classVisitor的抽象visitXXX方法将属性全部转发出去。
参考:
分为核心API和树形API
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:
ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。
可以把java代码转为ASM框架的代码。
参考:
ClassWriter是ClassVistor的实现类。
处理逻辑都写自定义ClassVisitor里。
模板的拦截代码如下:
1 | ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base"); |
代码示例:在一个方法前后分别插入方法
例如有一个类
1 | public class TestBean { |
要插入AopInterceptor的beforeInvoke()和afterInvoke()在TestBean的halloAop()的执行前后。
1 | public class AopInterceptor { |
在自定义ClassVisitor的visitMethod方法中拦截halloAop方法,对不是halloAop的方法返回null表示不处理,对halloAop的拦截处理交给AopMethod类
1 | class AopClassAdapter extends ClassVisitor { |
MethodVisitor中
visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
1 | class AopMethod extends MethodVisitor implements Opcodes { |
参考:
存放在栈帧的操作栈中。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。
参考
参考:
可以获取方法体内部的字节码指令等方法的一切信息
微信Android客户端卡顿检测工具:Matrix-Android-TraceCanary
遍历字节码指令,没有有效的字节码指令就是空
代码如下:
1 | private boolean isEmptyMethod() { |
1 |
|
1 | private boolean isSingleMethod() { |
对相同前缀的字符串进行了压缩存储,存储空间少,访问一个字符串最多也只需要访问字符串的长度。
利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的字符串比较,查询效率比哈希树高。
在搜索引擎中关键词提示,引擎会自动弹出匹配关键词的下拉框。
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
例如:
这样一方面可以节约存储空间,另一方面先用字典树预处理了海量文本,之后进行字符串查找时,不用在整个文本中查找特定字符串
给定很长的一个串,统计频数出现次数最多情况。
例如:
Trie树利用多个字符串的公共前缀来节省存储空间,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀,所以可以利用这个特点来解决一些前缀问题。
例如: