字节抖音客户端提前批一面20200712

设计模式有了解那些?

写单例模式的代码

Java的四种引用类型

进程和线程的区别

volitile关键字是什么意思

进程中的sleep/wait/join/yeild方法分别有什么作用

Android有了解吗?安卓上的MVC、M什么的有了解吗?

一个ArrayList<String str> 里面删除所有特定的字符串要怎么做

编程题:

求两个单链表的交叉点

常见排序算法的稳定性分析

稳定性简而言之就是带相同元素在排序前和排序后的相对位置是否保持一致。

默认从小到大的进行排序

1.冒泡排序

思想:每一轮都是将最大的一个元素冒到数组的结尾。

分析:当相邻的两个元素相等的时候,通常的做法应该是选择后面的那个相同元素继续做冒泡的比较,这样稳定性就不会被破坏。但是如果你在两个相邻元素相同的时,还是选择了交换两个相同的元素,这样稳定性就会遭到破坏,当然没有人会这么多此一举,所以通常默认情况下,因此冒泡排序就是一个稳定的排序算法。

2.选择排序

思想:每一轮选择最小的值所在的下边,确定之后交换到特定位置

分析:举个反例 5,4,5,3 排序,第一轮之后 3,4,5,5 。两个5的相对位置发生了变化,因此选择排序不稳定

3.插入排序

思想:维护数组前部分有序,当前元素插入到合适的位置。

分析:如果再向前找的过程中发现相同元素,则插再这个相同元素的后面,这样相同元素的相对位置不发生变化,是一种稳定的排序算法。但是如果你找到相同元素的时候,硬是要插入到前面,那么就是不稳定了,这里的稳定是默认你不会做这么无聊的操作了。

4.快速排序

思想:主元+分治

分析:举一个反例:3,2,2 第一轮3作为主元,变成 2,2,3 ,这样就可以认为排好序了。两个2的相对位置发生了变化,一种不稳定的排序算法。

5.归并排序

思想:分治与归并

分析:元素的交换只发生的归并的阶段,就是merge两个有序的小数组的过程。归并的过程中需要借助辅助数组。两个有序数组中,小的数排前面,当遇到相同的数的时候,则左边数组的数先排,这样就可以保持相对位置。因此是一种稳定的算法。如果你非要先排右边数组的元素,那么就破坏了稳定性,当然也是需要考虑到你不会这么做。

其他排序算法:

桶排序(基数、计数):稳定

堆、希尔:不稳定

上述提到的稳定算法基本上都是理想上可以达到的,当程序员的实现上不严谨的时候,会导致稳定的排序算法变成一种不稳定的排序算法。

工程中的排序算法

工程中类库提供的排序都是经过高度集成与优化的,时间复杂度O(NlogN)的算法都涉及到递归的操作,工程中是不允许出现递归,递归排序都被改写成了非递归的。

Java中的Arrays.sort方法会先判断数据的长度,如果数据在一定范围内,会直接使用插入排序,因为插入排序虽然是一个时间复杂为O(N²)的排序算法,但是如果数据量并不是很大,那么相对来讲很快,并且插入排序的特点就是如果一个数据近似有序,那么时间复杂度也近似O(N)。如果数据量较大,那么Arrays.sort方法会再判断排序的data是基础类型还是一个自己定义的对象,如果是基础类型,那么会使用快排,因为基础类型要保持稳定性是无意义的,如果是对象例如自己定义的类Student,那么就会使用归并排序,保证data的稳定性。无论是归并排序还是快速排序,当数据量被分解到一定范围时,会再次转换为插入排序 。

参考文献:

https://blog.csdn.net/wumenglu1018/article/details/106712801

https://www.jianshu.com/p/a5b5b5c4ee0f

C++ 复习 STL库

参照传智播客的黑马的教程 https://www.bilibili.com/video/BV1et411b73Z/?p=2

Standard Template Library(标准模板库)

STL从广义上分为:容器、算法和迭代器 (container、algorithm、iterator)

容器之vector

三种遍历方法

#include<vector>
vector<int> v;
v.push_back(10);
vector<int>::iterator itBegin = v.begin(); //第一个元素的地址(指针)
vector<int>::iterator itEnd = v.end();    //最后一个元素的下一个元素的地址(指针)
while (itBegin != itEnd) {
    cont << * itBegin << endl;
    itBegin++;
}

for (vector<int> ::iterator it = v.begin(); it != v.end(); it++){
    cout<<*it<<endl;
}
#include <algorithm> //标准算法头文件
void myPring(int val){ cout<<val<<endl;}
for_each(v.begin(), v.end(), myPrint); //这里采用了回调函数的技术

自定义类型的容器

vector<Person> v;
for(vector<Person>::iterator it = v.begin(); it != v.end(); it++){
    cout << (*it).name << endl;
}
vecter<Person*>v;
for(vector<Person*>::iterator it = v.begin(); it != v.end(); it++){
    cout << (*(*it)).name << endl;
    cout << (*it)->name <<endl;
}

容器可以嵌套

vector<vector<int>> v;
for (vector<vector<int>>::iterator outerIt = v.begin(); outerIt != v.end(); outerIt ++) {
    for (vector<int> ::iterator innerIt = (*outerIt).begin(); innerIt != (*outerIt).end(); innerIt ++ ) {
        cout << *innerIt << " ";
    }
    cout<<endl;
}

string类,底层就是通过char*来实现的(字符串的字面量实际上时const char * 类型的)

string的四种构造函数

  • string()
  • string (const char * c)
  • string(const & string str)
  • string(int n, char a)

string类内部重载=赋值运算符,还重载了多个,还有提供了assign以及重载的多个方法。

string类内部还重载+=等拼接字符串的方法,还提供了append一些列重载函数。

string类的查找函数find,rfind,一个从左往右找,一个从右边往左边查,并且重载了一系列。

string类的替换函数replace,详细函数的定义需要查查文档。

字符串比较 int compare(const & str)

字符串重载[]实现字符的读写,也提供了at函数提供访问。(这和java不同)下面两种都是合法的。

  • str[1] = ‘a’;
  • str.at(1) = ‘b’;

字符串插入删除 insert() erase()方法

字符串的字串 subStr()

vector

是一个动态的数组,有以下四种方式的构造函数

  • vector()
  • vector(v.begin(), v.end())
  • vector(int n, element)
  • vector(const vector & v)

vector容器可以赋值(重载了operator=),也可以调用assign方法

vector的size() capacity() empty() resize()方法

vector的插入和删除

push_back() pop_back() insert() erase()

vector元素存储 重载[]运算符,调用at(), front() 第一个元素,back()最后一个元素

vector容器的swap()方法

看得懂这个代码吗 vector<int>(v).swap(v)

调用拷贝构造函数创建匿名对象并实现vector的内容交换,一定程度上会节省v中capacity多余的空间浪费,而匿名对象会被系统回收。

reserve()函数,预留空间,减少动态扩容是的复制数据的开销。

deque容器

双端队列 #include<deque>

头部插入的效率高于vector,具体需要数据结构看底层源码

vector访问元素的速度会比deque快

API基本和vector类似,只是多了一些在头部的方法

deque没有capacity()方法,和底层实现有关(中控器)

//deque容器的排序
#include <algorithm>
sort(d.begin(), d.end())

stack容器

#include<stack>

stack<T> stk; stack<T>(const & stack stk); 普通构造和拷贝构造

pop push top 三个方法

size empty 方法

queue容器

#include<queue>

queue<T> que; queue<T>(const & queue que);

pop push front back 方法

size empty 方法

list容器

list表示链表(注意和java中的区别),c++底层实现是双向循环列表

多数的API和vector差不多,但是没有capacity的概念,不支持[] 和at访问,l.begin() 只能++或者– 不能 +=1 +=2 之类的(不支持随机访问)

反转和排序 list<int> l; l.reverse(); l.sort()

所有不支持随机访问迭代器的容器,不可以使用标准算法,(#include <algorithm>),一般来说这种容器的内部会提供对应的成员方法。

bool compare(int v1, int v2){ return v1 > v2; }
l.sort(compare);   //降序排序

自定义数据类型排序需要指定排序规则。

set/multiset

#include<set>

底层是用二叉树来实现的数据结构,是一种关联式容器

set不允许重复元素,multiset允许重复元素

插入元素只能用insert函数,删除erase,清空clear

find函数返回的是迭代器,count函数非1即0(multiset可能是其他值)

    set<int> s;
    pair<set<int>::iterator, bool> res = s.insert(100);
    if (res.second) {
        cout << "successful insertion!" << endl;
    } else {
        cout << "inserted failed!" << endl;
    }
    res = s.insert(100);
    if (res.second) {
        cout << "successful insertion!" << endl;
    } else {
        cout << "inserted failed!" << endl;
    }

set 的函数返回的结果是pair<iterator, bool> 第二个参数表示是否插入成功。

pair队组

两种创建队组的方式

  • pair<string, int> p(“Amy”, 17);
  • pair<string, int> p2 = make_pair(“Jerry”, 30);

cout << p.first << ” ” << p.second << endl;

set的排序(仿函数)

class MyCompare {
    public:
        bool operator()(int a, int  b) const{
            return a > b;
        }
};

int main() {
    cout << 'a' << endl;
    set<int, MyCompare> s;
    s.insert(1);
    s.insert(2);
    s.insert(3);
    s.insert(4);
    s.insert(5);
    for (set<int, MyCompare>::iterator iter = s.begin(); iter != s.end(); iter ++) {
        cout << *iter << endl;
    }

}

自定义数据类型装入set中必须在创建set的时候,指定仿函数比较器,或者在自定义类中重载比较运算符。

Map容器

#include <map> //一种关联式容器

map里面存的都是队组pair

int main() {
    map<int, int> m;
    m.insert(pair<int, int>(10,10));
    m.insert(make_pair(30,10));
    m.insert(pair<int, int>(20,10));
    for (map<int, int>::iterator iter = m.begin(); iter != m.end(); iter++) {
        cout << iter->first << " " << iter->second <<endl;
    }
}

empty size swap erase(重载多个) clear

查找元素

map<int, int>::iterator iter pos = m.find(key);

函数对象(仿函数)

特点

  • 函数对象在使用时,像普通函数一样调用
  • 函数对象超出普通函数的概念,可以有自己的状态
  • 函数对象可以作为参数传递

谓词

仿函数返回值是bool数据类型,称为谓词。(有些类似java中的函数式接口或者python中的filter)

一元谓词(有一个形式参数)–过滤

二元谓词(有两个形式参数) –排序

内建函数对象

算术仿函数

#include <functional>
negate<int>n; //一元
cout<<n(50)<<endl;
plus<int> q;  //二元
cout<<q(10,5)<<endl;

关系仿函数(greater 最常用)

#include<vector>
#include<functional>
vector<int>v;
sort(v.begin(), v.end(), greater<int>());  //关键在这里
for(vector<int>::iterator it = v.begin(); it != v.end(); it ++) {
     cout << (*it) << endl;
}

逻辑仿函数(基本用不到)

STL常用算法

  • #include<algorithm>
  • #include <numeric>
  • #include <functional>

遍历

for_each(v.begin(), v.end(), 函数或者仿函数);

搬运、转换(类似java8的stream或者python的列表表达式)

trandform(s.begin(), s.end(), d.begin(), 函数或者仿函数); //目标容器需要提前开辟空间

查找:

find (v.begin(), v.end(), int val); //自定义类型需要重写运算符==方法。

find_if(v.begin(), v.end(), 仿函数); //

adjacent_find 查找重复相邻元素的地址

binary_search(v.begin(), v.end(), int val);

count(v.begin(), v.end(), int val);

count_if(v.begin(), v.end(), 一元谓词);

排序

sort(v.begin(), v.end(), 二元谓词);

random_shuffle(v.begin(), v.end());//记得加随机数种子

merge(v1.begin(), v1.end(), v2.begin(), v2.end(), target.begin()); //target需要先开辟,v1,v2需要有序

reverse(v1.begin(), v2.begin());

拷贝和替换

copy(v1.begin(), v1.end(), target.begin()); //先开辟空间

replace(v1.begin(), v2.end(), int old, int new);

replace_if(v.begin(), v.end(), 一元谓词, int new);

swap(v1,v2);//同种类型的容器才能交换

算术生成算法

#include <numeric>
int total = accumulate(v.begin(), v.end(), int start_sum);
fill(v.begin(), v.end(), 100);

“集合”算法

set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), target.begin()) //target的容量需要先开辟,前提必须是两个有序的序列

set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), target.begin()) //target的容量需要先开辟, 要求有序

set_difference(v1.begin(), v1.end(), v2.begin(), v2.end(), target.begin()) //target的容量需要先开辟, 要求有序 ,差集有先后顺序关系

JAVA复习:GC垃圾收集器

学习内容来自尚硅谷宋红康老师的JVM教程

https://www.bilibili.com/video/BV1PJ411n7xZ/

垃圾是什么?指运行程序中没有任何指针指向的对象

不清理垃圾有什么后果?垃圾对象占用内存空间,容易内存溢出。

内存溢出和内存泄露的区别,

  • 内存溢出很容易来理解, 内存不够用,是指在程序申请内存时,没有足够的内存提供就出现了内存溢出
  • 内存泄漏 但是程序在申请内存后,无法释放已申请的内存空间 如java的数据库连接或者网络连接没有及时close。
  • 当内存泄露越来越多的时候,最终会导致内存溢出

JAVA 的垃圾回收机制

自动内存管理,(分配和回收)开发人员就不同过多关注内存的分配和释放,降低了内存的泄露和溢出。

java堆是垃圾回收的重点。青年区->老年区->永久代 回收频率依次减少

垃圾回收的算法

垃圾标记阶段:引用计数算法和可达性分析算法

引用计数器算法,每个对象保存一个整形的应用计数器,记录对象自身被引用的次数。

优点:实现简单,垃圾对象容易识别,判定效率高,回收没有延迟

缺点:额外的空间,更新计数器额外的时间开销。无法处理循环引用的情况(内存泄露,致命的缺点,因此java没有采用)

可达性分析/根搜索/追踪性垃圾收集算法

对象的finalization机制

对象要被gc回收之前会执行finalize方法,定义在Object类中的方法。可以在里面重写一些资源释放的方法。

但是不要主动去调用这个方法。

对象的三种状态

清除阶段的三种算法:

  • 标记-清除算法
  • 复制算法
  • 标记-压缩算法

标记-清除算法

标记从Gc roots开始遍历,标记所有可达到的对象(在对象的Header中记录为可达对象)

对堆中所有对象进行遍历,清除没有标记的对象。

缺点:效率不高,在进行GC的时候,需要停止整个应用程序,影响用户体验(Stop the world),容易产生内存碎片。需要维护一个空闲列表。

清空就是将对象的地址记录到空闲列表上。

复制算法:

优点:没有标志和消除过程,高效,不会产生碎片问题

缺点:需要两倍内存空间,需要维护对象之间的引用关系,如果大部分对象都不是垃圾,移动对象的时间开销也会很大

标记-压缩(标记-整理)算法

复制算法高效性的前提是垃圾对象多,存活对象少。而这很符合年轻代中数据的特点。但是老年代就不一样了,老年代的数据大部分是存活数据。

优点:没有内存碎片,不需要维护空闲列表,比复制算法空间利用率高

缺点:效率低,要维护对象间的引用,需要STW,影响用户体验

对比

分代收集算法:具体问题具体分析

青年代用复制算法,同时设置伊甸园区和幸存者区缓解内存的浪费。

老年代用标记-清除和标记-整理混合的方法。

增量收集算法:

缺点:造成垃圾回收总体成本提高,系统吞吐量下降。因为切换线程和上下文转换有额外开销。

分区算法:

一些概念:

System.gc() /Runtime.getRuntion().gc(),可能触发Full GC,对老年代和青年代进行回收。但是无法保证立刻调用垃圾收集器,会在程序执行的某个不确定时间节点进行调用。

System.runFinalization(); 会强制调用失去引用对象的finalize方法。

内存溢出 OOM:没有空闲空间,并且垃圾回收之后还是不够。

内存泄露 Memory Leak:对象不会再被程序使用了,但是GC又无法回收了。通常是编码过程的疏忽,导致某些对象的生命周期过长。

内存泄露的两个例子。

STW stop the world 所有的垃圾收集器都会有这个情况。就是把所有用户进程停止,执行垃圾回收。

并发(concurrent)实际上就是同步,某个时刻只有一个程序在执行,只是CPU快速地切换,让我们感觉同时在执行。

并行(Parallel) 同一个时刻,多个程序在真正运行。

  • 垃圾回收的并行:多条垃圾回收线程并行执行。
  • 垃圾回收的串行:只有一个垃圾回收器线程执行。
  • 垃圾回收器的并发:用户线程和垃圾回收线程同时执行。

安全点:

安全区域:

引用(前提都是可达的对象引用)

强引用对象不会被gc回收,也是内存泄露的主要原因。

软引用,内存足够不会回收,内存不够时,在二次gc时被回收。

弱引用:被gc就回收

JDK更新可以从三个层面区关注:

  • 语法层面
  • API层面
  • 底层优化

垃圾回收器的分类:

  • 按线程数分:串行gc和并行gc
  • 按照工作模式:并发式和独占式
  • 按碎片分:压缩式和非压缩式
  • 按工作内存:年轻代gc和老年代gc

GC的性能评估指标

上面标红的三项是主要的,但也是互相制约的。

主要关注前两项

吞吐量与暂停时间的矛盾。

可以在JVM参数中设置Parallel GC的吞吐量或者暂停时间的参数。

JAVA中的数组、链表、队列、栈

首先说一下Java中的集合框架collection是一个接口,、

下面有两大类型的子接口,分别是List和Deque

List下有三大常见的实现类,ArrayList、LinkedList、Vector

其中 ArrayList、Vector 底层都是用数组来实现的, LinkedList底层是用链表实现的

ArrayList非线程安全,Vecotr虽然是线程安全的但是已经被弃用了。

Stack类是早期JAVA为了栈数据结构而设计的一个类,它是vector的子类,但是早期设计的时候考虑的不是特别的完美,因为父类vector是一个线程安全的,意味着效率比较低,同时Vector的上层接口List里面有一个add(index, Element)的方法,被调用会破坏stack的结构,就是在这栈的中间可以随意的插入元素。因此Stack这个类也被弃用了。

Deque 接口顾名思义就是为了实现队列而提供的接口。双端队列。

下面有很多有用的实现类,ArrayDeque、LinekList、PriorityQueue 里面也封装了很多有用的方法。

LinkedList即使List的实现类又是Deque的实现类(底层是链表)因此可以使用它来作为队列或者栈来进行使用。

虽然LinkedList的功能很强大,但是它也是List的实现类,避免不了被错误调用

破坏了栈只能在栈顶操作数据的原则

操作队列或者栈用的方法主要带有offer poll peek + First/Last,而最好不是用add/remove之类的方法或者push/pop之类的方法,容易搞混乱。

这篇文章讲得很好。

https://mp.weixin.qq.com/s/Ba8jrULf8NJbENK6WGrVWg

复习:深度底层理解Java的String

学习内容来自尚硅谷宋红康老师的JVM教程

https://www.bilibili.com/video/BV1PJ411n7xZ/

String的两种创建方式:

  • String s = “abc”; //字面量初始化
  • String s = new String(“abc”); //对象的形式初始化

String类是final的,不可继承

JDK8以及以前版本String的底层是用char[]来进行存储的

JDK9以及以后底层改变为了用byte[]数据进行存储

修改底层结构的字符:String在程序中很常用,并且字符中大部分是拉丁文,即用一个字节就能进行存储,而cahr类型是两个字节的,因此会造成内存空间的浪费。

char[]就改变成了byte[]和对应的编码类型

StringBuilder、StringBuffer、JVM底层处理String的方式自JDK9都做了相应的修改。

String代表不可变的字符序列。

字符串常量池中不会存储相同的字符串。

StringPool 实际是一个固定大小的Hashtable,不能动态修改,常量过多的话Hash冲突会增加。大小可以在JVM的参数上进行定制。

常量池就类似java系统级别的一个缓存,可以让运行速度更快、更节省内存。

JDK6 的时候 字符串常量池在永久代的常量池中,JDK7和JDK8之后都在堆中。

为什么调整?

  • 永久代默认空间比较小,容易溢出
  • 永久代的垃圾回收的频率比较低(或者不回收),字符串回收效率低,也容易溢出

进阶知识

String s = “a” + “b” + “c”;

String s = “abc”

这两句在编译之后是等价的,常量字符串拼接(包括final修饰的变量)是在编译时期进行优化的。即在生成的字节码文件里面是两个一样的字面量“abc”

如果字符串的拼接符号的前后出现了变量,相当于在堆中new了一个String对象。

  • String s1 = “ab”;
  • String s2 = “cd”;
  • String s3 = s1 + s2;
  • String s4 = “abcd”;
  • System.out.println(s3 == s4); // false
  • String s3 = s1 + s2;这一句代码查看底层的字节码可以解释为
    • String tmp = new String();
    • tmp.append(s1);
    • tmp.append(s2);
    • s3 = tmp.toString();

以上导致了对象引用和常量池引用的地址不一致问题。

        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = s + i;
        }
        
        StringBuilder s1 = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            s1.append(i);
        }

上述代码展示的两种字符串拼接方案,效率千差万别

  • append的方式只有一个StringBuilder对象,而字符串拼接的方式每次拼接都会产生一个新的StringBuilder对象以及String对象
  • 其次 ,第一中方式内存中存在过多stringBuilder和String垃圾,如果进行gc需要花费额外的时间。

进一步提高,StringBuilder可以在初始化的时候指定char[]数组的容量,可以避免扩容过程中带来的性能损耗。即如果逻辑大致确定,可以指定一个capacity的上界,就不会扩容了。

intern函数,返回字符串是字符串常量池中的地址(有就直接返回,没有就“创建”,JDK版本有差别)

new String(“ab”); 这条语句会创造几个对象?

一个对象是new 关键字创建的,另一个对象是字符串常量池中的对象。

new String(“a”) + new String(“b); 呢?从字节码的角度看一共有5个对象

  • new StringBuilder()
  • new String()
  • 字符串常量池中的对象“a”
  • new String()
  • 字符串常量池中的对象”b”

StringBuilder的toString方法中 return new String(value, 0, count);分析对应的字节码,得知并没有在常量池中创建“ab”,

字符串常量池的HashTable中放到是什么?现在的理解存到的就是一个完整的String对象的引用,该对象引用和字面量引用是等价的。而new出来的对象是在堆中的。

面试题:

String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);           //false

String s3 = new String("1") +  new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);       //jdk6:false   jdk7/8:ture

第1部分比较简单,就是s1在对空间中创建了一个字符串一的对象,同时字面量一也在字符串常量池里面创建了一个一的对象,而第2句s1.intern();就相当于什么都没做,第3句的S2通过字面量初始化,指向字符串常量池中的那个对象,所以两个地址是不一样。

第2题就比较难,因为它的输出结果和jdk的版本有关,执行intern语句之前,如果字符串常量池中已经有了,就没什么区别。当字符串不存在常量池中的时候,intern方法在1.6中会在永久代中复制一份String对象,并返回相应的地址。而在JDK7/8中,字符串常量池的位置调整到了堆中,调用intern方法的时候如果常量池中没有,则直接指向堆中已有的那个对象,节省空间。

String s = String.valueOf(123);
System.out.println(s == s.intern());    //jdk8 true
String s3 = new String("1") +  new String("1");
String s4 = "11"; 
s3.intern();
System.out.println(s3 == s4);       //jdk7/8:false
String s1 = new String("a") +  new String("b");
String s2 = s1.intern(); 
System.out.println(s1 == "ab");    //jdk6:false   //jdk7/8:true
 System.out.println(s2 == "ab");   //jdk6:true    //jdk7/8:true

在代码中何时的位置加上intern函数可以在一定程度上节省内存空间。因为字符串常量池中的数据可以复用,重复的String对象就可能被gc回收。

复习:Java虚拟机 JVM底层结构

学习内容来自尚硅谷宋红康老师的JVM教程

https://www.bilibili.com/video/BV1PJ411n7xZ/

类的加载过程

  • 加载
    • 通过一个类的全类名获取该类的字节流
    • 将字节流代表的静态结构转换为方法区的运行时数据结构
    • 在内存中生成一个Class对象,作为这个类的各种数据的访问入口
  • 链接阶段
    • 验证:验证字节流中的信息满足虚拟机要求,如cafebabe头
    • 准备:为类变量分配内存,类的赋值默认初始值(非final的静态变量)
    • 解析:将常量池中的符号引用转换为直接引用的过程
  • 初始化:执行类构造器方法clinit方法(如给静态变量进行显式初始化, 静态代码块执行),子类运行前需要先加载父类的clinit方法,该方法是同步的。

类的加载器

两种类型,引导类加载器(Bootstrap Classloader)和自定义类加载器(拓展类加载器,系统类加载器,用户自定义加载器)

  • 引导类加载器在代码中获取不到,使用c、c++实现的,嵌套在jvm内部,主要加载一些java的核心类库,也会加载拓展类加载器和系统加载器
  • 拓展类加载器加载,父类加载器就是引导类加载器,在ClassLoader继承体系下,加载jre/lib/ext下面的jar包
  • 应用程序类加载器(系统类加载器), 父类加载器就是引导类加载器 , 在ClassLoader继承体系下, 是默认的类加载器,我们在代码中写的类都是这个加载的。

用户自定义类加载器在一些特殊情况下使用

双亲委派机制

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
  • 如果父类的加载器还存在父类的加载器,则进一步加上委托依次递归请求最终达到顶层的启动类加载器,
  • 如果父类加载器可以完成内加载任务就成功返回,倘若腹内加载器都无法完成此任务,此类加载器才会自己尝试去加载,这就是双亲委派模型

反向委派,核心类库是接口,外部jar包是实现类。

优势:避免类的重复加载,保护程序安全,防止核心的API被修改

沙箱安全机制,就是双亲委派机制对核心源代码的保护,不能再核心类库包名下定义main方法。

运行时数据区

本地方法栈、虚拟机栈、程序计数器、堆区、元数据区(方法区/非堆空间)

线程有自己的程序计数器、虚拟机栈、本地方法栈

多个线程共享堆区和方法区(进程独占,生命周期同JVM)

一个JVM实例对应着一个Runtime类的对象。

程序计数器(PC寄存器)

主要用来存储指向下一条指令的地址,也就是即将执行的指令代码,由执行引擎读取下一条指令

线程私有的,生命周期和线程同步,占用内存很小,不会发生OOM错误而且没有GC。

PC寄存器存储地址有什么用呢?因为CPU会不停的切换各个线程,当切换为某个线程之后,就要知道程序从哪里继续执行。

虚拟机栈

JAVA指令是根据栈来设计的,这样的优点是可以跨平台,指令级比较小,编译器容易实现,但是缺点是和基于寄存器的指令性能实现了一定的下降,同样的功能也需要更多的指令。

栈是运行时单位,堆是存储的单位。

虚拟机栈里面存数的是很多的栈桢 stack frame,每一个栈桢对应着一个方法的 。栈顶的栈桢对应着当前方法。

栈是一种快速有效的分配存储方式;访问速度仅次于PC计数器;操作只有两个 ,入站、出站;栈不存在垃圾回收问题.

StackOverFlowError请求的容量超过了虚拟机占允许的最大容量(无限递归)

OutofmemoryError动态拓展虚拟机内存的时候无法申请到足够的内存

JVM栈可以在启动的时候制定大小。

栈中存什么,栈桢(一个内存区块),栈桢和方法是一一对应的关系

当前栈桢–当前方法–当前类

方法的结束方式有两种,第一种正常结束return;第二种方法中出现未捕获的异常,以抛出异常结束。因此有可能栈顶是异常结束,栈底正常结束,因为栈底处理了异常。

每个栈桢的结构有:局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息

局部变量表是一个数字数组,存方法参数和方法体内的局部变量,包括基本数据类型、引用数据类型和返回值类型。不存在安全问题。局部变量表的大小是在编译时期确定的。

局部变量表中的slot(除了long 和double占用两个slot(32bit 一个 slot),其他类型的数据或者引用占用一个slot);成员方法的局部变量表中的第一个元素是this的引用。slot重复利用,作用域小的销毁后重复利用。

成员变量都有默认初始化的过程,而局部变量没有,所以局部变量必须显式地赋值。

操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间,

在方法执行过程中,根据字节码指令往栈中写入数据或提取数据即入栈或出栈

说操作数栈实际底层肯定是要用具体的数据结构来实现的,然后可以用数组或者列表,这里用的数组。所以需要指定数组的大小,编译时候确定。

动态链接(指向运行时常量池(这个常量池是字节码文件内部的,最终也会放到常量池中)的方法引用)的作用就是将符号引用转换为调用方法的直接引用

为什么需要常量池?提供一些符号和引用便于指令识别,减小字节码文件的冗余,多个字节码文件可以共用方法区的常量池。

方法调用是怎么样的?方法调用也有静态链接和动态链接对应早期绑定和晚期绑定。就是多态。

Lambda表达式引入入让java这种静态类型语言具有了动态类型语言的一些特性以及底层是用invokeDynamic指令来实现的。用值判断变量的类型,而不是用变量类型来约束值的类型是动态类型语言的关键。

虚方法和非虚方法

虚方法如果每次都需要不断向上层父类找实现类,效率会比较低。为了提高性能,建立虚方法表。虚方法在类加载的链接阶段被创建(具体是在解析的阶段)

方法返回地址 调用者的pc计数器的值作为返回地址,即调用方法之后的下一条指令地址。

方法如果正常退出,则正常返回调用者的方法返回地址,如果是异常退出,调用者则根据异常表来定位接下来执行的位置。

一些附加信息 不重要

一些面试题:

栈溢出的情况:stackOverflowError

调整栈的大小,能保证不一出吗?否

垃圾回收设计JVM栈吗?否

分配栈的内存越大越好吗?否

自定义的局部变量是否线程安全?不一定

如果方法的形参是引用类型的变量或者返回值是引用参数类型,则可能是线程不安全的

本地方法接口和本地方法库—–非运行时区的模块

本地方法:Native Method就是一个Java调用非Java代码的接口。为什么要用本地方法?

  • Java应用需要与Java外界环境交互,效率考量
  • Java需要与操作系统的交互,调用c语言实现的接口

本地方法栈

类似于虚拟机栈的作用,就是管理本地方法的调用,也是线程私有的,内存溢出差不多,有StackOverflow和OutOfMemory,也可以设置栈的内存大小。

一个JVM实例只有一个堆内存,堆也是java内存管理的核心区域。Java堆积在juem启动的时候即被串件,其空间大小也就确定了,是JVM 管理最大的一块内存空间。所有的线程共享堆空间(但是堆中可能有线程的私有缓冲区TLAB)

几乎所有的对象实例和数组都在堆空间中,对象的引用在JVM栈中的某个栈帧的局部变量表中。方法运行结束的时候,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

堆是垃圾回收的重点区域。

现在垃圾收集器大部分都基于分代收集理论设计

  • JDK7以及之前:新生区+养老区+永久区
  • JDK8以及之后:新生区+养老区+元空间
  • 永久区/元空间可以理解为方法区。主要先关注新生区和养老区

堆可以手动设置堆空间的大小,默认是电脑内存的1/64,最大的堆内存大小是电脑内存的1/4。建议初始和最大的设置成一样的。

OOM Error 堆中的对象所占的空间超过了堆的大小,则会导致堆空间的溢出。

年轻代和老年代(二者的内存比例可以分配,默认值是1:2)

年轻代有分为:伊甸园区、幸存者1区和幸存者2区(默认比例是8:1:1,但是需要显式指定)

几乎所有的java对象都是在Edan区创建的,大多数的数据都在青年代被销毁。

  • 创建对象的过程,创建的对象默认放在edan区,当edan区满的时候,触发gc,不是垃圾的全部到to区,from区的也会被gc检查,不是垃圾也到to区。
  • from区和to区的数据会维护一个年龄,每次gc之后年龄+1,超过阈值的时候移动到老年区。
  • from区或者to区满的时候不会触发gc,只有edan区满的时候会,当伊甸园区来的对象to区放不下的时候,可以直接放到老年区。
  • 如果老年区也放不下,在老年区执行一次GC,如果还是放不下就报错OOM。

YGC就是MinorGC,MajorGC是OGC/老年代的GC。

GC线程执行会导致用户进程的暂停,所以GC要尽可能少。

分代的原因就是通过统计数据优化垃圾收集器的性能。因为大部分的对象的生命周期都比较短。

TLAB Thread Local Allocation Buffer

堆是线程共享数据,但是存在线程安全问题,加锁会导致效率的下降,因此未每个线程在伊甸园区分配私有缓存区域。(通常这个空间比较小,为伊甸园区的1%。)

伊甸园区过大,会降低MinorGC的效率,过小会导致MinorGC触发的频率过高,影响用户线程,所以设置要平衡一下。

堆是分配对象存储的唯一选择吗?(”是“)

逃逸分析后,如果未逃逸,则可以将对象优化成被栈上分配。

如果new的对象可以在方法外被进行调用,则发生了逃逸。(就是一个作用域的问题)

  • 栈上分配可以在某种程度上来说,加快代码的执行效率,并减少垃圾回收执行的次数,增强用户线程的体验。
  • 同步消除,如果锁通过逃逸分析,仅仅在当前线程被使用,那么不存在同步安全问题,可以消除同步操作。
  • 标量替换,未逃逸的对象,可以从聚合量的形式被打散成标量(基本数据类型),直接在栈上进行分配就可以了。

但是逃逸分析本身也要消耗性能的,总体的提升是有风险的。该技术还不成熟

方法区

方法区是线程共享的,逻辑上是堆的一部分,但是在大部分JVM在实现过程中是和堆分开的。jdk7 叫做永久带,jdk8叫做元空间。

方法区和永久代不严格等价,在hotspotJVM上是等价的。

元空间的使用的内存是本地内存,永久代用的是虚拟机内存。

方法区主要存类型信息,常量,静态变量和JIT代码缓存

类信息:

  • 类的完整定义如类声明、继承、实现情况、修饰符等
  • 域信息(成员变量):名称、类型、权限修饰符、顺序
  • 方法信息:方法名称、返回值类型、参数个数类型、方法字节码、本地变量表、操作数栈、异常表……

全局变量和全局常量的区别,全局常量在编译阶段就赋值完成了(在字节码文件中有体现),而全局变量在类加载链接阶段的prepare阶段才默认初始化,在第三各初始化阶段才显示初始化。

运行时常量池:字节码文件中有常量池,方法区中有运行时常量池。

常量池包括了各种字面量和对类型、域和方法的符号引用。


为什么要改?

  • 永久代的空间是不好确定,容易产生OOM
  • 对永久代的调优的过程是比较困难的

为什么StringTable要调整?因为永久代回收的效率很低,在full gc 的时候会触发,这导致StringTable回收的效率不高,而日常开发中会有大量的字符串被创建,如果不及时回收,会导致永久代空间不足,放到堆里面,回收会比较即使。

区分好对象本身一直是在堆区的,而对象引用变量(存的对象地址)放的位置不同版本有区别。

方法区的垃圾回收主要是常量池中废弃的常量不再使用的类型

方法区中类的回收条件十分苛刻,费力不讨好。

创建对象的方式

  • new
  • Class.newInstance
  • Constructor.newInstance
  • clone(实现Cloneable接口以及浅复制)
  • 反序列化

创建对象的步骤:Object obj = new Object();

对应的字节码:

Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object.””:()V
7: astore_1
8: return

  • 判断对象对应的类是否加载、链接、初始化
  • 为对象分配内存,计算占用空间的大小(确定的)
    • 内存规整-指针碰撞(整块空间)
    • 内存不规整-空闲分配列表(碎片化的空间)
  • 处理并发问题
  • 初始化分配到的空间(赋默认初始化值)
  • 设置对象头(所属的类,对象的HashCode,对象的GC信息、锁信息)
  • init方法初始化

对象的内存布局:

  • 对象头Header
    • 运行时元数据:哈希值、GC分代年龄、锁状态等
    • 类型指针:指向类元数据
  • 实例数据:从祖宗到自己的各种类型的字段
  • 对齐填充

对象的访问定位:

  • 句柄访问:效率低(间接访问),需要额外内存维护句柄池,栈中的指针稳定,
  • 直接指针(HotSpot采用的方式):效率高,栈中的指针不稳定,节省内存

直接内存

Java Process Memory = Java heap + native memory (其他空间太小了)

执行引擎

解释器和JIT编译器->java是半编译半解释型语言(与javac编译无关)

解释器:效率比较低,但是响应时间快

为什么非要有字节码?不想C语言、C++一步到位?分层思想可以提高各个部分的效率,不用一步到位的考虑,就想OSI网络分层结构一样,应用层不用直接管物理链路层的东西。虽然分层做出了一定性能上的牺牲。

JIT(Just in time)编译器:响应时间慢,但是执行效率高

Java中的编译器:前端编译器、后段编译器

热点代码以及探测方式

JIT编译后的代码缓存在方法区

默认情况下JVM执行引擎是混合模式,可以通过参数制定只运行特定模式。

JIT编译器分为C1(客户端)和C2(服务端)两种

Java 复习 JDK8 新特性 Lambda 和 Stream

参考自视频:https://www.bilibili.com/video/BV18b411t7Nc/

下面复习JDK8主打的两种新特性 Lambda 和 Stream

Lambda表达式

Lambda在一些动态语言中是一个匿名函数,在java中Lambda本质上是一个接口的实例化对象

格式 (o1, o2) -> Integer.compare(o1,o2)

  • 左边是形参列表(重写抽象方法的形参列表)
  • ->是Lambda操作符
  • 右边是Lambda方法体(重写抽象方法的方法体)

六种调用方式

  • Runnable runnable = () -> {System.out.println(“running”);}
  • Consumer<String> consumer = (String str) -> {System.out.println(str);}
  • Consumer<String> consumer = (str) -> {System.out.println(str);}
  • Consumer<String> consumer = str -> {System.out.println(str);}
  • Comparator<Integer> comparator = (o1, o2) -> return Integer.compare(o1, o2)
  • Comparator<Integer> comparator = (o1, o2) -> Integer.compare(o1, o2)

总结:是参数的类型可以省略,有自动推断机制,如果参数只有一个,那么括号可以省略,你把方法体应该用一对大括号来扩充,但是如果其中呢只有一条语句的话,括号可以省略,如果只有一条语句,并且是return语句,return这个关键词也可以省略。

这些都是建立在实现的接口中只有一个抽象方法(不会有歧义),这种类型的接口叫做函数式接口

函数式接口可以用FunctionalInterface注解进行修饰,类似Override注解的功能,在编译时期进行格式检查。

Lambda表达式一定依赖于函数式接口。

四大函数式接口

方法引用和构造器引用

方法引用本质上就是lambda表达式,只是一种更简便的方法,

使用要求:使用方法引用的前提是已有方法实现了接口抽象方法实现的功能,形参和返回值可以匹配上,有如下三种情况:

对象::非静态方法

Consumer <String> consumer = System.out::println;

Supplier<String> supplier = emp::getName;

类::静态方法

Comparator<Integer> comparator = Integer::compare;

Function<Double, Long> fun = Math::round;

类::实例方法(这种比较难)

BiPredicate<String, String> pre = String::equals;

Comparator<String> comparator = String::compareTo;

Function<Employee, String> func = Employee::getName;

构造器引用

Supplier<Employee> su = Employee::new;(调用空参数构造器)

Function<Integer, Employee> fun = Employee::new; (调用带一个参数integer的构造器)

BiFunction<Interger, String, Emplyee> fun2 = Employee::new;

数组引用(把数组看作一个特殊的类)

Function<Integer, String[]> fun = String[] :: new;

Stream API 真正把函数式编程风格引入到Java中,可以对集合数据进行操作,类似于SQL执行数据库查询。集合是数据在内存中,Stream是计算在CPU中

Stream不会自己存储元素,Stream也不会改变原有对象,Stream的计算是延迟的,

Stream的三个步骤,创建对象、中间操作、终止操作。

创建stream对象的方法

Collection接口中的default方法 stream 和 pararallStream

Arrays类的静态方法 stream

Stream类的静态方法 public static stream of (T…t)

创建无限流

Stream.iterate(0, t->t+2).limit(10).forEach(System.out::println) 输出前10个偶数,从0开始

Stream.generate(Math::random).limit(0).forEach(Sytem.out::println)输出10个随机数

中间操作

筛选和切片

fileter(Predicate p) 自定义筛选数据

limit(n) 截断流,取前n个数据

skip(n) 跳过前n个数据

distinct 筛选 利用流中元素的hashCode和equal方法去除重复元素。

映射

map(function fun)

Stream<String> stream = Arrays.asList("aa","bb","cc").stream();
stream.map(String::toUpperCase).forEach(System.out::println);

flatmap 扁平映射,把Stream套Stream的形式压成一层扁平的Stream

排序 sorted 自然排序和 sorted(Comparator comparator)定制排序

终止操作:

匹配和查找

allMatch anyMatch noneMatch 见面知意,参数一定是Predicate接口

findFirst、findAny、count、max(Comparator com)、min(Comparator com)

forEach(Consumer c) 内部迭代

归约 reduce(T identity, BinaryOperator)

System.out.println(Stream.iterate(1, x -> x + 1).limit(10).reduce(Integer::sum));

收集

collect(Collector c)

stream.collect(Collectors.toList()) stream.collect(Collectors.toSet())

Optional类 是用来避免空指针对程序对伤害。Optional可以理解为是一个容器或者是一个包装类,Optional有两种构造方法,一种是Optional.of(T) 一种是Optional.ofNullable()。为了避免在程序中出现空指针异常,使用orElse(备胎)方法获取Optional容器中的对象,如果是null就返回备胎,保证返回的对象不空。后面JDK9、10、11还会对这个类进行优化,可以暂且不用太关注。

java 高级复习

参考自 https://www.bilibili.com/video/BV1Qb411g7cz?from=search&seid=7373469111232659208

程序:是为了完成特定的目的,用某种语言编写的一种指令的集合

进程:是程序的一次运行后者是一个正在运行的程序,进程是资源分配的基本单位

线程:线程是进程的进一步细分,是程序内的一条执行路径。线程是调度和执行的单位,每个线程……

在java的虚拟机JVM中,每个线程都有自己的虚拟机栈和程序计数器,方法区和堆是进程独有的。即多个线程共享进程中的方法区和堆,这就存在安全隐患。

单核cpu上的多线程是假的多线程,只是一种快速的线程切换,同一个时刻只能执行一个线程。多核CPU才是真的多线程。

一个java应用程序至少有三个线程,main()主线程,gc()垃圾回收线程,异常处理线程。

并行(单核,时间片)与并发(多核)

单核CPU多线程实际比单线程慢,线程切换要花时间,但是多线程有他自己的优点,提高应用程序的响应,提高用户体验。提高计算机系统CPU的利用率。也可以改善程序结构,不用将冗长复杂的进程分为多个线程独立运行,利于理解和修改。

何时需要多线程 ?需要同时执行多个数据 (多功能的引用)或者需要一些等待的任务(加载图片) 或者一些后台运行的程序(垃圾回收)。

创建线程的两种方法:

方法一;继承Thread类并重写run方法,new出对象之后调用start方法。只能start一次,否则会出异常或者重新new一个对象再调用start方法。也可以用匿名子类。

    new Thread(){
        @Override
        public void run() {
            System.out.println("test");
        }
    }.start();

Thread.currentThread()一个静态方法,返回当前执行代码的线程。

yield()方法:释放当前线程的cpu执行,但是有可能下一时刻又拿到执行权。

join()方法:在线程A中调用线程B的join方法,线程A进入阻塞状态,直到线程B完全执行完以后,A结束阻塞状态。

sleep()方法:阻塞当前线程具体的时间

isAlive() 判断当前线程是否还存活。

线程有优先级1-10,默认是5

第二种创建线程的方法,实现Rnnable 接口,并重写run方法。

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test");
}
}).start();

两种方式优先选择Runnable的方式,应为实现的方式没有单继承的限制,更适合用来处理共享数据的问题。两种方法都需要重写run方法,将线程要执行的逻辑写在其中。

线程分为两类:守护线程和用户线程

线程的生命周期,状态:新建、就绪、运行、阻塞、死亡

线程的同步为了解决线程安全问题,原因在于操作共享数据为完成时,其他线程也进来操作数据。

同步代码块 synchronized(同步监视器){},同步监视器就是锁,任何一个类的对象都可以充当锁,要求多个线程共用同一个锁。解决了线程安全问题,但效率上有一点损失。在Runnable实现的方式可以用this(代表当前对象)作为锁。继承Thread的方法不可以,可以用类对象充当锁,Test.class。

同步方法如果一个操作共享数据的代码在一个方法中,直接用同步方法就可以。非静态同步方法的默认持有锁是this,Runnable方式好用,静态方法的同步监视器是当前的类对象,继承Thread方式好用。

用线程同步解决懒汉式单例模式的安全隐患。将getInstance方法直接声明为同步方法,但是效率不高。提高效率如下:

class SingleTon {
    private SingleTon(){}
    private static SingleTon instance;
    public static SingleTon getInstance(){
        if (instance == null) {
            synchronized (SingleTon.class){
                if (instance == null)
                    instance = new SingleTon();
            }
        }
        return instance;
    }
}

死锁,不同线程分别持有对方所需要的同步资源,都在等待对方释放资源,形成了死锁。

JDK5新增两种解决线程安全的方式,Lock锁。(ReentrantLock lock = new ReentrantLock();)也要保证里lock的唯一性。

lock.lock() lock.unlock() 夹住同步代码,可以结合try 和 finally

sychronized 和 lock的异同。两者都可以解决线程安全问题,sychronized自动释放锁,lock需要手动释放锁。lock的方式更灵活,其次是同步代码块,再是同步方法。

线程的通信涉及三个方法

wait()方法,当前方法进入阻塞状态,并释放锁

notify()方法,唤醒被wait的优先级最高的一个线程

notifyAll()方法,唤醒所有被wait的的线程

tip:三个方法必须用在同步代码块或者同步方法中,调用这三个方法必须由锁对象进行调用。这三个方法都定义在Object类中。

sleep方法和wait方法的异同:都可以时当前线程进入阻塞状态,两个方法声明的位置不同,sleep在Thread类中,wait在Object类中,sleep可以在任何需要的时候调用,wait必须在同步方法或者同步代码块中调用。如果两个方法都使用在同步代码中,wait方法会释放锁。

消费者和生产者问题的多线程设计

创建线程的方法三:实现Callable接口,结合FutureTask和Thread类。

     FutureTask futureTask = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                return null;
            }
        });
        new Thread(futureTask).start();
        try {
            Object obj = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

Callable接口比Runnable方法好?

  • call方法有返回值,可以利用futureTask任务的get方法取得
  • call方法可以抛出异常,被外面的操作捕获,获取异常的信息
  • callable支持泛型

创建线程的方法四:线程池(避免频繁的创建和销毁线程,e.g. 安卓listview的item图片加载。实现重复利用) 好处:1.提高响应速度2.降低资源消耗3.便于线程管理

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        });
        executorService.shutdown();

创建多线程的四种方式

方法导致状态变化(多线程可以作为例子),状态变化导致某些方法执行(回调方法,安卓activity中很常见)

java 常用类

String 是一个final类,不能被继承。实现了serializable和comparable接口,可序列化,可比较。内部定义了final char[]进行存储字符串数据。String代表不可变的序列。

Stirng可以通过字面量的定义方式,String对象引用指向的内容是存在jvm方法区的常量池中。而常量池会维护其中常量的唯一性。重新赋值,连接操作,或者修改操作时,都是在常量池中进行新建一个字符串常量的。

String两种实例化方式的区别。new 的方式如果常量池中没有定义的话是创建了两个对象

        String a = "abc";
        String b = new String("abc");
        System.out.println(a == b); //false
        System.out.println(a.equals(b)); //true
        System.out.println(b.equals(a)); //true

常量和常量(final 修饰的变量)的拼接,结果在常量池且常量池中不会有相同的内容。

只要有一个变量,那么结果就在堆中。除非结果调用了intern方法,结果就在常量池中。

        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = "a"+"b";
        String s5 = s1 + "b";
        String s6 = "a" + s2;
        String s7 = s1 + s2;
        String s8 = (s1 + "b").intern();
        final String s9 = "a";
        String s10 = s9 + "b";
        System.out.println(s3 == s4); //true
        System.out.println(s3 == s5); //false
        System.out.println(s3 == s6); //false
        System.out.println(s3 == s7); //false
        System.out.println(s3 == s8); //true
        System.out.println(s3 == s10); //true

JVM内部结构是随着版本进行优化,也会有不同企业公司面对不同落地需求,优化虚拟机内部结构版本的JVM。有名的就是sun公司的java hotspot JVM

Jvm规范中将堆分别(青年区、老年区和永久区),其中永久区可以认为就是方法区。

JDK6 常量池在方法区,JDK7的常量池在堆里,JDK8常量池有回到永久区,但是永久区名称叫成元空间了。

字符串的常用方法很多,需要铭记一点String是不可变的,任何产生字符串改变的方法都不会改变原来的字符串,都是生成新的对象。

String与基本数据类型的转换,parseXXX与valueOf方法。

String与char数组类型转换 String.toCharArray() 与 new String(char [] arr))

String与Byte数组的转换 String.getBytes() —— new String(byte[]) 使用特定的字符集编码将字符串转换为byte数组,abc之类的就是ASCII值,如果字符串中出现了中文,utf-8会将中文转换为3为的byte,gbk转换成2位字节。

编码:字符串(看得懂)->字节(看不懂) 解码:字节->字符串

乱码的出现就是由于编码和解码的字符集不同。

String、StringBuffer 和 StringBuilder的区别。

  • StringBuffer类:可变的字符序列,线程安全,效率低。 底层用char[]实现
  • StringBuilder类:可变的字符序列,线程不安全,效率高。 底层用char[]实现
  • String类:不可变的字符序列,底层用char[]实现

StringBuffer默认初始化16个长度的char数组,或者初始化长度+16,每次扩容为原来的2倍+2,或者最小扩容数量。也可以在初始化的时候定制初始化长度。如果应用场景需要频繁修改字符串,StringBuffer的效率肯定高于String。

单线程效率:StringBuilder > StringBuffer > String

StringBuffer的一些方法返回值是this,可以实现方法链调用,这些方法对StringBuffer自身对象进行了修改,如append,delete等方法。返回值是String的方法则对本身不进行修改,如substring方法。

StringBuilder只是多数方法上加了sychronized 关键字。

java.util.Date 是 java.sql.Date的父类,后者和数据库交互的时候用,

java.util.Date转换为java.sql.Date类型的对象

simpledateformat类的构造函数,format和parse方法

jdk8 的localdatetime、instant和datetimeformatter这三个类关于时间日期操作更好用,符合人们认知和使用习惯

如果对象的引用需要能够用> >= < <=等进行比较时,需要使用Comparable接口,实现类需要重写compareTo方法,实现一种自然的排序。compareTo方法当前对象小返回负数,相等返回零,否则返回正数。这种比较方式实现(集成)在类的内部,类在定义的时候就需要制定,不够灵活。

Comparator接口也是一种比较器,可以实现定制排序。实现类需要实现compare方法。

        String [] arr = {"bac", "acd", "ddk", "bcd"};
        Arrays.sort(arr, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -o1.compareTo(o2);
            }
        });
        System.out.println(Arrays.toString(arr));

两种方式对比,Comparable接口一旦制定,类在任何时候都可以比较。Comparator具有临时性。

System类代表系统,构造器是私有的,成员变量和方法基本都是静态的。如out,currentTimeMillions()、gc()……

Math类提供了一系列静态方法用于科学计算

BigInteger可以表示不可变的任意精度的整数。BigDecimal表示数字精度比较高的数字。

定义一组常量的时候,可以使用枚举类。

自定义枚举类可以利用final关键字实现,jdk5 以后可以使用enum关键字,更方便了。

enum Season{
    Spring("春天"),SUMMER("夏天"),AUTUMN("秋天"),WINTER("冬天");
    private String season;
    private Season(String season){
        this.season = season;
    }
}

注解 Annotation 注解是一种趋势

框架 = 注解 + 反射 + 设计模式

java中的注解

生成文档的相关注解如@author @version @param等

编译时进行格式检查 @Override @Deprecated @SuppressWarnings

如果方法上有@Override注解的话,编译过程会去校验方法的定义是否符合重写的规则,否则会报错,编译无法通过。@Deprecated 过时方法,但能用,为了兼容老代码。@SuppressWarnings 抑制编译器的一些警告,如变量没有使用,没有声明泛型等。

跟踪代码依赖性,代替配置文件的功能 servelet spring junit等框架提供的注解

注解可以认为是和类、接口、枚举并列的结构。也可以自定义。

注解具体实现什么功能?需要通过反射机制来完成。自定义的注解必须配合具体信息处理流程才有意义。

元注解是用来修饰其他注解的,主要有四种

Retention 表示所修饰注解的生命周期,取值Source表示在编译之前生效,class表示在会生成字节码文件(默认),runtime表示会被类加载器所加载到运行中,通过反射生效处理流程。

Target用于制定注解可以修饰的结构,不写默认都可以用,如类、接口、枚举、方法、构造器、变量、局部变量等

Documented表示所修饰的注解可以被解析到javadoc中

Inherited表明注解有继承性

通过反射可以获取注解信息,例子

Class clazz = Student.class;
Annotation [] annotations = clazz.getAnnotations();

jdk8 中关于注解的新特性

这里理解不是很深,后期再回来看看吧

集合框架

java从服务器数据库读取的list,为啥用json传,因为json是字符串,字符串是可序列化的。

集合框架分为collection接口 (collection又分为list接口 和set 接口)和map接口两个系列

list表示有序可重复集合,即动态数组,set表示无序不可重复集合

向实现collection接口的类对象中添加自定义类的时候,这个自定义类最好实现equals方法。因为很多方法都需要比较元素。

ArrayList.contains(), containsAll, romove, removeAll(删除交集),retainAll(求交集)方法比较的是对象的内容,调用equals方法。两个new的相同的字符串会返回true,如果自定义类没有重写eqauls方法,调用OBject类中的==比地址,则返回false。

数组和list的转换

  • Collection coll = Arrays.asList(12, 34, 56);
  • Object [] obj = coll.toArray();

Iterator迭代器接口(设计模式的一种,提供访问容器中各个元素的方式,而又不暴露容器的细节),通常使用hasNext方法和next方法结合使用。内部定义了Iterator在遍历的同时,remove删除当前元素。这比for循环靠谱,因为动态删除的时候数组的长度变了。

JDK 5 增强for循环,用于遍历集合对象(collection)和数组。底层就是用迭代器实现的。

for (Object obj : coll) {} 

List接口的三种实现类

  • ArrayList:作为List接口的主要实现类,线程不安全,效率高,底层用Object[]存储
  • LinkedList:底层使用双向链表进行存储,对频繁对插入和删除操作效率高
  • Vector:List接口的古老实现类,线程安全,效率低,底层用Object[]存储

ArrayList源码分析JDK7:

默认创建长度为10的Object[](饿汉),扩容每次为原来的1.5倍。建议new的时候制定容量,扩容移动数据会消耗资源。

JDK8中默认创建为{},第一次调用add操作的时候(懒汉),才创建长度为10的数据,后续其他操作 一致。

LinkdedList源码分析:定义了私有静态内部类Node,first和last属性,Node内部有prev和next两个引用,双向链表。

Vector默认创建10,扩容两倍,但是通常已经不用这个类了。

List常用方法,增删改查插长度遍历

注意list中remove方法的重载,以及list底层的Object数组,多态以及自动装箱。

ArrayList<Integer> al = new ArrayList<>();
al.add(1);
al.add(2);
al.add(3);
al.remove(2);
System.out.println(al); // 1,2
al.remove(new Integer(2));
System.out.println(al); //1

Set接口的主要实现类(无序,不可重复)

  • HashSet:作为Set的主要接口实现类, 线程不安全,可以存储null值
  • LinkedHashSet:作为HashSet的子类,遍历内部元素时,可以按照添加的顺序遍历‘
  • TreeSet类:可以按照添加元素制定的属性进行排序

Set接口没有额外定义新方法,都是Collection中的。

无序性理解:是不按照添加的顺序,不是随机性。底层虽然是用数组实现的,但是添加到数组中的位置是有hashcode决定的

不可重复性理解:保证添加的元素用hashcode和equal方法判断时,不能返回true

HashSet底层时用数组实现的,向其中添加元素时,首先算元素的Hashcode值,在通过散裂函数算存储的位置,如果该位置没有元素,添加成功。如果有元素,先比较hash值,不同则添加成功,相同再比较equals方法,false则添加成功。

多个元素通过链表进行存储,JDK7 和 JDK8中 (JDK8中旧的在原位)

向Set中添加的元素,需要重写hashcode方法和equals方法,相同元素一定要有相同的hashcode。

HashSet的底层是用HashMap来实现的。

LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据额外维护了两个prev和next指针(引用),对于比较频繁的遍历操作,效率高于HashSet,空间换时间。并且遍历顺序是按照添加顺序。

TreeSet中添加的元素,要求是相同类的对象。添加的对象必须有比较的方法,实现comparable接口或者定制comparator,定制的newTreeSet的时候传入。底层用红黑树进行实现。用campare/campareTo方法的返回值判断是不是唯一元素,不再用equals方法了。

List 和 Set 中都有很多方法涉及到重写equals方法,除了TreeSet.

一道有难度的题目,考察hashset中add方法的底层实现

public class HashSetTest {
    public static void main(String[] args) {
        HashSet<Person> hs = new HashSet<>();
        Person p1 = new Person(15, "Tom");
        Person p2 = new Person(15, "Amy");
        hs.add(p1);
        hs.add(p2);
        p1.setName("Jack");
        System.out.println(hs);
        hs.add(new Person(15, "Jack"));
        System.out.println(hs);
        hs.add(new Person(15, "Tom"));
        System.out.println(hs);

    }
}

class Person{
    private int age;
    private String name;

    Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person persino = (Person) o;
        return age == persino.age &&
                Objects.equals(name, persino.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

Map接口 双列数据,存储key-value对的数据。

Hashmap是map的主要实现类,非线程安全,可以存null的key和value,LinkedHashMap是hashMap的子类,提高了遍历的效率,且遍历顺序按照添加顺序。

Treemap内部元素按照key排序(自然、定制),底层用红黑树。

Hashtable古老的实现类,线程安全,效率低,不能存null,不常用了。有一个子类是Properties,配置文件,key value都是String

Hashmap JDK7 数组+链表 JDK8 数组+链表+红黑树

Map内部结构的理解:

key 无序,不可重复,value 无序,不可重复。key-value pair用Entry对象进行存储。key对象需要重写hashcode和equals方法。value要重写equals方法。

Hashmap的底层实现原理

初始化长度为16的Entry[] table.

put元素的时候,key.hashcode 散裂(& length-1) 确定位置,没冲突则装成Entry放入。冲突,则对比hashcode,在比key.equals(方法). 一样修改,不一样添加(头插法)。

每次扩容为原来的2倍。超过threshold并且索引位置不为空的时候扩容。

JDK8中Node[],而不是Entry[](本质一样,改个名罢了),首次put的时候才创建(懒汉),底层结构多了红黑树(数组+链表+红黑树)。当一个索引位置上元素个数大于8,且数组长度大于64,转换成红黑树,提高查找效率。五大常量:

  • DEFAULT_INITIAL_CAPACITY
  • DEFAULT_LOAD_FACTOR
  • threshold
  • TREEITY_THRESHOLD
  • MIN_TREEIFY_CAPACITY

LinkedHashMap就是把Node类改造了一下(继承),加了两个指针(引用)。

HashSet底层就是用Hashmap实现的,只存key,value统一指向一个静态的Object类的对象。

hashMap的遍历方法,keySet() / values() / entrySet() + iterator/增强for

面试题:负载因子的大小对Hashmap有什么影响?

负载因子表示Hashmap的数据密度。太大会怎样,太小会怎样?

treemap需要key是同一种类型的对象,并且要求该类实现comparable接口

Arrays是操作数组的工具类,Collectiions是操作集合和Map的工具类。

collection和collections的区别。

Collections常用方法包括reverse、shuffle、sort、swap、max、min(可以定制Comparator)、frequency、copy

        List src = Arrays.asList(new Integer[]{1,2,3});
        List des = Arrays.asList(new Object [src.size()]);
        Collections.copy(des, src);

Collections.sychronizedXxx方法返回线程安全的List、Set和Map,底层就是用同步代码块包裹了一下原来的方法。

数据结构的真实结构(数组、链表)和抽象结构(线性表(数组、链表、栈、队列)、树、图、其他),抽象结构都是在真实结构的基础上进行构建的。

泛型可以和集合框架结合,保证集合内部数据类型的一致性和类型检查,避免了强制转换带来的潜在异常。还可以用在Iterator、Comparator等接口,很方便。

Set<Map.Entry<String, Integer>> entrySet = hm.entrySet();

泛型必须是类,不能是基本数据类型,泛型不制定,则默认是java.lang.Object.

自定义泛型,继承的时候子类可以指明父类的泛型,子类就不是泛型类了。如果子类不指明,则子类还是泛型类。静态方法不能用泛型(从生命周期的角度很好理解),异常类也不能用泛型。

泛型方法:在方法中出现了泛型的结构,与类的泛型没有关系,类不是泛型类也没关系。泛型方法可以是静态的。

public <E> List<E> copy(E[] arr){} //public 后面的 <E> 是为了防止编译器认为E是一个类

例子:DAO(data access object)数据访问对象 就可以定义泛型,DAO中定义操作数据库表的共性操作的方法,T就可以是数据库中的表对于的类。

泛型在继承方面的体现。

类A是类B的父类,G<A>和G<B>不具有子父类关系,而是两个并列的结构。这会导致某些操作不太方便,因此引入通配符,二者的共同父类是G<?>

通配符: ?

public void print(List<?> list) {
Iterator <?> iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
}

像上面写不太好,不能再像list中添加任何元素了,除了null值。因为?具体表示什么类型不确定,只能读了,并且读出来的元素应该复制给Object类。

有限制的通配符。源码中很常见

?extends Person 实际上就是小于等于,这种方式添加数据不行

?super Persion 实际上就是大于等于,这种方式添加数据OK的

文件与IO流

文件File类,IO流主要有InputStream、OutputStream、Reader、Writer四个基本抽象类,任何其他的流操作都是在这四个类上继承而来的。

inputstream和outputstream是字节流,reader和writer是字符流

输入和输出都时相对与内存来说的

流可以分为节点流(例如直接作用在文件上)和处理流(作用在流上,如buffered)

流通常需要处理关闭close,否则可能会有内存泄露问题

FileReader作用于File节点,可以用char[]数组存读取的数据

FileWriter 文件不存在会自动创建,默认是覆盖模式,可以在File构造的时候制定为append模式。

FilieReader和FileWriter不能复制二进制文件(如图片等),因为是字符流

FileInputStream和FileOutputString可以复制二进制文件,字符文件当然也可以。读的过程用byte[]字节数组进行存储。

缓冲流是一种处理流,内置缓冲区,可以加速读写操作

BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

字符流可以直接使用简单的方法,String line = br.readline(), 不包含换行符, bw.write(line) bw.newline()

转换流也是一种处理流,InputStreamReader和OutputStreamWriter,从后缀来看属于字符流,可以将字节输入流转换成字符流,将输出字符流转换成字节流。

转换流就设计到了编码解码

new InputStreamReader() 默认是用系统默认的字符集进行解码,当然可以显式地指定字符集。

字符集:

ASCII码(8位)、ISO8859-1(8位,欧洲)、GB2312(中文,最多两字节)、GBK(中文、最多两个字节,首位是0就是一个英文字母字节,是1就是两个字节中文)、

Unicode(所有语言的字符集,两个字节,实际没有落地) Unicode的问题,1.英文一个字节表示就够了,纯英文文件的话,文件有两倍大。2.如果英文的字符用一个字节进行编码,那么怎么区分一个字节字符和两个字节字符?3..如果英文采用首位0标识,那么能编码的字符数量就少了一半,不够用。

UTF-8(UCS transfer format)-8表示每8位传输。变长的字节编码方式,Unicode的一种实现方案,用前缀表示字符的长度,0xxxxxxx 、110xxxxx 10xxxxxx、1110xxxx 10xxxxxx 10xxxxxx、 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

ANSI编码标准,英文系统用 ISO8859-1 编码,中文系统默认用GBK。

标准输入输出流 默认是键盘输入控制台输出。可以通过方法重定向流,setIn,setOut。

System.in 是InputStream, 可以先包转换流,再包缓冲流,然后用readline读取System.out是PrintStream

打印流PrintStream和PrintWrite都是输出的,提供了一系列的重载的print()和println()方法。可以配合System.out实现输出的重定向

数据流DateinputStream和DateOutputStream是为了方便处理基本数据类型或者字符串。当然dos写到文件的数据对人来说通常是不可读,应该用dateinputstream进行配套对应读取。这两个类没什么意思

装饰设计模式和代理设计模式的区别。

对象流。 ObjectOutputStream,序列化 ObjectInputStream 反序列化。需要保证对象所属于的类是可序列化的。

对象序列化机制:允许把内存中的加了对象转化成平台无关的二进制流,从而允许把这种二进制流持久的保存在磁盘上或者通过网络,将这种二进制流传到另一个网络的一个节点上。当其他程序获得了,这种二进制流可以恢复成原来的java对象。

自定义类需要实现序列化Serializable接口,并提供serialVersionID值并且类中所有的属性都应该是可序列化(String和基本数据类型是可序列化的)才可以序列化。

serialVersionID理解: 就是一个类的标识,如果不显示指定,虽然会自动生成,但是如果你修改了类反序列化的时候可能不会保存,显式制定后再修改类反序列化的时候不会出问题。

static transit 成员变量不可序列化。

RandomAccessFile类,可以读可以写,写内容的时候可以从头覆盖,原来内容多可能留下来。

new RandomAccessFile(new File(), mode) mode可以是r rw rwd等

内部有位置指针seek(), 如果要实现插入的功能就比较麻烦了。多线程断点续传。

NIO(New IO、None-blocking IO)是面向缓冲区的,基于通道。是一种更加高效的读写文件的方式。Path类(可以当成File类的升级版)Paths类, Files类。

网络编程两个问题:如何定位主机以及主机上的应用?(IP和端口)如何进行可靠高效的数据传输?(网络通信协议)

IP区分主机,端口号区分应用

网络通信协议:OSI参考模型(理想7层,无法落地)TCP/IP参考模型(4层,应用层、传输层、网络层、数据链路层,实际实现的网络通信协议)

IPv4、IPv6、公网IP、私有IP。IP(不容易记忆)与域名(容易记忆,通过DNS服务器解析)

本地回路地址:127.0.0.1——-localhost

端口号和进程挂钩(程序/应用),同主机端口不能冲突。

在Java中ip和端口被封装在Socket类里面。

TCP和UDP是两个传输层协议,TCP建立连接三次握手,断开连接四次挥手,是可靠的

UDP不用建立连接,不可靠连接,效率比TCP快,e.g.直播

java开发中可以使用Socket做客户端,ServerSocket做服务器。

read是阻塞式方法,客户端可以调用socket.shutdown()方法通知服务器以及发送完数据了,不用再等了。

UDP的话用DatagramSocket和DatagramPacket类就行了。

URL 协议+主机名+端口+路径+参数 URL类 HttpURLConnection类

Java反射

反射是动态语言的关键,允许程序在执行期间取得类的任何内部信息。甚至是私有的结构都可以。java实际上是静态语言,有了反射,使java称为准动态语言。

反射机制和封装性的矛盾。实际上不矛盾,封装是设计上的思想,私有实际是内部使用,不推荐外部使用。

通过直接new和反射的方式都可以调用公共结构,应用场景各别是?

编译的时候无法确定要创建哪一个对象,可以用反射。WEB应用中很常见,因为用户的请求总是动态的,而服务器早就在运行态了。

对Class类的理解:类的加载过程,javac后会生成多个.class字节码文件,java.exe会对某一个类进行解释执行,将某个字节码文件加载到内存中。次过程为类的加载,加载到内存中的类称为运行时类,是Class类的一个实例。加载到内存中的运行时类,会缓冲一定时间,在此时间内,可以通过不同的方式获取此运行时类。

获取Class的实例的方式。方式三最常用,方式1写死了,方式2逻辑不对,先有对象

        //调用运行时类的属性
        Class clazz1 = Person.class;
        //通过运行时类的对象的getClass方法
        Person p = new Person("Amy", 20);
        Class clazz2 = p.getClass();
        //调用Class的静态方法
        Class class3 = Class.forName("java.lang.String");
        //使用类的加载器 Test是当前类
        ClassLoader classLoader = Test.class.getClassLoader();
        Class class4 = classLoader.loadClass("java.lang.String");

Class的实例可以是那些结构,类、接口、注解、数组、基本数据类型、void、枚举

如果程序主动使用某个类,如果该类还未被加载到内存中,则系统会通过如下三个步骤对内进行初始化。Load、Link、Initialize。

可以用ClassLoder系统类加载器加载配置文件,和Property的文件流加载方式的功能一样,默认路径在工程下的src目录。

Class的方法,newInstance()调用空参数构造函数(有权限 public)创建对象。

通常在javabean中要求提供一个public的空参构造器,便于通过反射,创建运行时的类,同时便于子类继承时,子类调用super()。

运行时类的属性结构:

getFields():获取当前类和父类中声明为public的属性

getDeclaredFields(): 多去当前类中声明的所有属性

getMethods(): 获取当前运行时类和父类中声明的所有public方法

getDeclaredMethods():获取当前类中声明所有方法

获取方法声明的注解(框架中很常用): method.getAnnotations()

还可以获得运行时类的带有泛型的父类的泛型,在DAO有用到

class StudenDao extends Dao<Student>{}

获得运行时类的注解 clazz.getAnnotations() 注解应该是RUNTIME

获取运行时类实现的接口 clazz.getInterfaces() 在动态代理中可以用。

通过反射操作实例的属性:field.set(对象,name)

Class clazz = Person.class;
Person p = new Person();
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
name.set(p, "Tome");

通过反射调用实例的方法:field.set(对象,name)

Class clazz = Person.class;
Person p = new Person();
Method show = clazz.getDeclaredMethod("show");
show.setAccessible(true);
show.invoke(p);

创建类的对象的方式:

  • new + 构造器
  • XXX XXXs XXXFactory XXXBuilder中的静态方法
  • 反射

动态代理

静态代理:代理类和被代理费都要实现同一套的接口,所有的功能都由代理类来完成,当代理内完成不了的时候就用被代理类的方法。如房屋中介和租房子的人,他们时都要实现找房子的这个接口,而房屋中介就是代理类,租房子的人就是被代理类。又比如明星和经纪人的关系,他们都要实现明星这个结果,你的明星所有的事物都是由机器人来完成的,当明天要上课的时候,直接就调用明星的唱歌方法。

静态的意思就是,房屋中介和租房子的人在编译之前就应该写好两个类的代理关系,明星和经纪人也是一样的,如果一个工程上的代理的代理对的关系比较多,那么工程下的类的数目就会变得非常多。

下面这个动态代码挺有意思的


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        Human proxy = (Human) ProxyFactory.getProxyInstance(superMan);
        proxy.eat("beef");
        System.out.println(proxy.getBelief());
    }
}

interface Human{
    String getBelief();
    void eat(String food);
}

class SuperMan implements Human{

    @Override
    public String getBelief() {
        return "help others";
    }

    @Override
    public void eat(String food) {
        System.out.println("eat " + food);
    }
}

class ProxyFactory{
    public static Object getProxyInstance(Object obj) {
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
        myInvocationHandler.bind(obj);
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),myInvocationHandler);
    }
}

class MyInvocationHandler implements InvocationHandler{
    private Object obj;
    public void bind(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理中");
        return method.invoke(obj, args);
    }
}

输出:

代理中
eat beef
代理中
help others

动态代理和AOP(Aspect Orient Programming, 面向切片编程)

上面代码中的invoke方法的System.out.println(“代理中”);可以简单理解为一个切片。

Java 基础复习

https://www.bilibili.com/video/BV1Qb411g7cz?from=search&seid=7373469111232659208 参考的视频,整理出有用的一些点。

三种注释方式

变量的类型 基本数据类型 引用数据类型(数组、类、接口)

数组是数组引用类型的,操作数组的类是Arrays,里面实现了很多有用的方法

数组的静态初始化和动态初始化 new int[10]; / new int [] {1,2,3};

二维动态初始化必须指定第一维度大小 new int [10][];

jvm 中创建数组基本的堆栈结构变化

数组创建默认初始值 0/ false/ null

自动类型提升,强制类型转换

位运算中左移<< 右移>>(正数补0负数补1)是乘二除以二对关系(除二下取整数) 无符号右移>>>都补0

用异或运算实现两个数交换的骚操作,异或运算(或位运算)的一些性质对算法题有点帮助,如hashmap 里面hash值存入那个位置用的是(length-1)加&为运算,而length始终是2的幂。

局部变量在栈区,new 出来的东西在堆区

… 表示可变个数的形参, 接受0,1,2,…个参数,实际就是当做数组来处理。也可以直接传一个数组进去。 public void accept(String … agrs) {}

赋值的值传递,区别基本数据类型的传值和引用数据类型传地址

引用数据类型中的string字符串类型是比较特殊,由双引号表示的一个字符串,在内存中是存在一个常量池中,底层应该是用差数组来实现的,然后string在账中的变量存储的实际上就是一个地址值,之所以叫常量池,就是因为是不允许更改的,所以你新建一个string类型的字符串,实际上就是在常量池中又创建了一个字符串。所以字符串作为函数的形参的时候会和其他引用类型的变量体现出不一样的特性。但是作为参数的时候仍然符合直传递的一个特性。

封装性,private私有化不对外界暴露的成员变量、方法、初始化方法(e.g.单例模式)

private (类内可用)、缺省(同一个包可用)、protected(不同包的子类可用)、public 四种权限

可以用来修饰 属性、方法、构造器和内部类,修饰类(不包括内部类)只能用public和缺省。

类的属性初始化顺序

  • 默认初始化
  • 显式初始化 / 代码块(谁写在前先执行谁)
  • 构造器初始化(默认构造器的权限同类的权限)

this表示当前对象,可以修饰或者调用成员变量,属性以及构造器

this(形参列表),不能调用自己,并且必须在构造器的首行调用

package 表示类或接口所属的包,包名的命名规则和标识符相同,每.表示一层dir,同包名不能命名同名的类或者接口。import导入包名+类名,*导入包下的所有类。

java.lang包下的类可以不import,同包下的调用不用写import。异包同类只能用全类名的方式调用。import static java.lang.Math.*; import + static 调用类中的静态结构(属性或者方法)这个不常用。

继承可以避免冗余代码,易于功能扩展,是多态的基础。java中只允许单继承和多层继承。java.lang.Object是所有类的直接或者间接父类。子类可以重写覆盖父类方法

区别方法的重载和重写

重写时函数名参数列表都要相同,权限修饰符不小于父类被重写方法,父类private方法不可以被重写。返回值类型是void和基本数据类型是,重写方法也一致,如果是引用数据类型A,则重写方法的返回值可以是A或者A的子类,父类方法的异常也一样,重写方法抛出的异常也是一样或者子类。

super和this有点类似,可以修饰类的属性、方法和构造器。可以在子类的构造函数的第一行调用super(形参类表)的方式调用父类的构造器,和this()只能二选一。不写就会有默认的super(),子类不显示写构造方法就会自动调用父类的空参数构造器。

一个类如果有n个构造器,最多n-1的首行是this,至少一行是super。默认不写就是super。

子类的实例化过程,子类一定会直接或者间接的调用父类的构造器(n-1与1的关系),父类进而调用祖父类的,直到object。所以子类对象会继承所有父类的属性和方法。

多态,父类引用可以指向子类对象,父类引用只可以调用父类的方法,如果子类重写了该方法,则调用子类重写的。适用于方法,不适用于同名属性。

多态体现在执行的时候,属于一种动态绑定,是编译时期无法进行确定的。重载属于静态绑定,编译时期就可以确定,重写则无法在编译时期确定,解释运行时才可以确定,属于晚绑定。

instanceof关键字的使用,避免向下转型的时候出现异常

Object是所有类的直接或者间接父类,其中定义的属性(没有定义属性)和方法具有通用性

== 与 equals()的区别

==是一种运算符号

如果两边是基本数据类型,比较两边数值是否相同,int可以和double,char可以和int进行比较,除了布尔类型不能比较。

如果两边是应用类型的变量,则比较地址值是否相同,即是否是同一个对象。

equals是一种方法,不能使在基本数据类型上,在Objejct中类的定义是return(this == obj); 和==没有区别。

但是某个类重写了equals方法,则根据多态的动态绑定,执行的语句是重写的函数体。注意字符串和常量池的问题。equals中用getclass 比较会更严格 和 instance of 会存在漏洞(导致不同类对象,存在继承关系,返回的结果是true)。

to String 方法在object中默认返回类名和哈希值,子类可以重写,在print的时候被调用

基本数据类型和包装类,六种是number子类,自动装箱和拆箱

自动装箱与integer 缓存 -128-127

static关键字,静态变量随类加载,在JVM的方法区中的静态域里面放着

静态方法,随类加载。静态只能调用静态,非静态方法内部随便调用。

单例模式要求,私有化成员函数,唯一实例为静态,getInstance函数对外暴露,也为静态方法,分为懒汉式(类加载快但需要同步处理)和饿汉式(类加载慢但线程安全)两种方式。

main方法是程序入口,是一个静态方法,args参数接受的实际是命令行参数。

代码块(初始化块),不常用,就是一个大括号包裹的代码块,分静态和非静态。静态代码块随着类的加载而执行 (可存在多个,依声明顺序执行) 。非静态代码块随着对象的创建而执行,因此可以实现初始化功能。

类的属性初始化顺序:默认初始化、(显式初始化、 代码块(谁写在前面先执行谁))、构造函数

类的加载顺序,先加载父类(静态代码快)再加载子类(静态代码块),然后new 对象时先调用父类的非静态代码块和构造方法,再是子类的。

final关键字,可以修饰类,方法和变量。

修饰类则此类不能被其他类继承,不能有子类了。(e.g. String System StringBuffer类)

修饰方法则不能被重写,如getClass方法。

修饰属性就变成常量了,不能再修改。如果是属性只能用显式初始化/代码块/构造器中初始化

修饰局部变量,尤其是修饰形参时,不能再修改

static fianl 修饰属性,表示一个全局常量。

abstract 可以修饰类和方法。

abstract修饰类的时候,该类不能实例化,该类的非抽象子类可以实例化。

abstract修饰方法,e.g. public abstract void test(); 有抽象方法的类一定是抽象类。抽象类不一定有抽象方法。

若子类重写了父类的所有的抽象方法才可以实例化,否则子类也是抽象类。

abstract 不能和static、private 和 final共用。

抽象类的匿名子类对象

Person p = new Person(){
    @override
    public void eat(){
        System.out.println("吃饱饱");
    }
};

模版设计模式是抽象类的一种应用场景,实际中很多地方用到。

接口interface是和类class并列的一种结构,克服了类只能单继承的缺点,一个类可以实现多个接口。接口是一种规范,是多态的体现。

如果说类的继承是 is a 的关系, 实现接口更像 has a / can do 的关系,即具有某种特性或者功能。

interface中只能定义public final static 常量 和public abstract 抽象方法,不能有构造方法(JDK7 及之前)。

java类通过implement关键字取实现某个接口,该类需要重写interface中所有的抽象方法,否则该类退化为一个抽象类。接口之间可以多继承。应用场景e.g.JDBC面向接口编程。

接口的匿名实现对象和抽象类的匿名子类的书写形式差不多。

抽象类和接口的区别?…抽象类有构造器,接口没有。单继承与多实现。

接口与代理模式和工厂模式

一个类继承的类和实现的接口中有同名属性,类中调用要明确指出调用哪一个,如super. x 或者A. x

接口在JDK8中还可以有静态方法和默认方法

静态方法只能通过接口.静态方法名调用,接口实现类的对象无法调用,内部也访问不到,有点像把接口弄成工具类的意思。

默认方法就是一个普通的方法, 只可以通过实现类的对象进行调用。如果子类重写了,则调用重写的方法(多态)

interface Test{
    public final static int x = 0;
    public abstract void test();
    public static void test1(){}
    public default void test2(){}
}

如果父类和接口的默认方法出现同名,而子类没有重写,则优先调用父类的同名方法,类优先原则

如果两个接口有同名的默认方法,一个类实现这两个接口,并且没有重写,则调用该方法会报错,因为不知道调用哪个。如果非要调用:接口名.super.方法名()。

这里的知识点有点琐碎,看看得了。

内部类分为:成员内部类(静态、非静态),代码块内部类,局部内部类

以成员内部类为例,类内可以定义属性方法构造器等, 可以用final修饰,表示类不能被继承 ,abstract修饰表示不能实例化。

同时作为一个成员,可以调用外部来的一些属性和方法,可以被权限修饰符修饰

使用内部类使用频率很低,稍作了解。

局部内部类中调用局部变量时,局部变量应该声明成final,在安卓开发中比较常见。

Error不能显式处理,JVM内部出错,如栈溢出(无限递归),OOM(堆溢出)等

Exception分为编译时异常和运行时异常。常见运行时如角标越界,空指针,除零异常,类转换异常等。

两种处理方式,try-catch-finally (finally可选)和 throws。catch中的异常声明要按照先子后夫的顺序,否则unreachable。e.getMessage() e.printStacktrace()两个常用方法。

finally中的语句是一定会被执行的代码,即使catch里面又有异常或者代码在之前return了。fianlly里面有return会return final 里面的值。一般在finally写一些关闭资源的操作。

通常值处理编译时异常,否则无法通过编译,出现运行时异常的话,就是代码有问题,需要debug该代码了。

throws 是向上级抛异常,写在函数的参数列表后面。

子类重写父类方法是抛出的异常不能大于父类抛出的异常。

几个具有顺序依赖的方法要执行,而各个方法都可能抛出异常,这是各个方法不能各自用trycatch,而要throws,然后整体进行trycatch处理。

手动抛出异常关键字 throw new Exception(“msg”); 也可以自定义异常类等。