java 知识点
Object 有哪些方法
public
方法:getClass
、equals
、hashCode
、toString
、wait
、notify
protected
方法:clone
、finalize
private
方法:registerNatives
,该方法作用是将不同平台C/C++实现的方法映射到Java中的native
方法
public class Object {
private static native void registerNatives();
// 声明后有紧接静态代码块
static {
registerNatives();
}
}
自动装箱
public static void main(String[] args) {
int i = 0;
Integer j = new Integer(0);
System.out.println(j == i);
System.out.println(j.equals(i));
}
上述代码的输出是
true
true
Java 虚拟机 GC 根节点的选择
Java通过可达性分析来判断对象是否存活。基本思想是通过一系列称为”GC roots”的对象作为起始点,可以作为根节点的是:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
笔者这么理解,作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。
虚拟机栈、本地方法栈这都是局部变量,某个方法执行完,某些局部使用的对象可以被回收。
类加载机制
- 启动类加载器( Bootstrap ClassLoader)启动类加载器无法被 java 程序员直接引用, 这个类加载器负责把存放在
<JAVA_HOME>\lib
目录中的, 或者被-Xbootclasspath
参数指定路径中的, 并且是被虚拟机识别的类库加载到虚拟机内存中. - 扩展类加载器(Extension ClassLoader)负责加载在
<JAVA_HOME>\lib\ext
目录中的, 或者被java.ext.dirs
系统变量所指定的路径中的所有类库。 - 应用程序类加载器( Application ClassLoader )这个类加载器是
ClassLoader
中的getSystemClassLoader()
方法的返回值, 一般称其为系统类加载器, 它负责加载用户类路径(ClassPath
)上所指定的类库
从 java 虚拟机的角度而降, 只存在两种不同的类加载器:
- 一个是启动类加载器(
Bootstrap ClassLoader
), 这个类加载使用 C++ 语言实现, 是虚拟机自身的一部分; - 另一种是其他所有的类加载器, 他们由 java 语言实现, 独立于虚拟机之外, 并且全部继承自
java.lang.ClassLoader
加载类的寻找范围就是 JVM 默认路径加上Classpath
, 类具体是使用哪个类加载器不确定。
类加载主要步骤
- 加载 把 class 文件的二进制字节流加载到 jvm 里面
- 验证 确保 class 文件的字节流包含的信息符合当前 jvm 的要求 有文件格式验证, 元数据验证, 字节码验证, 符号引用验证等
- 准备 正式为类变量分配内存并设置类变量初始值的阶段, 初始化为各数据类型的零值
- 解析 把常量值内的符号引用替换为直接引用的过程
- 初始化 执行类构造器
<clinit>()
方法 - 使用 根据相应的业务逻辑代码使用该类
- 卸载 类从方法区移除
双亲委派模型
除了顶层的启动类加载器之外, 其余的类加载器都应当有自己的父类加载器, 父子关系这儿一般都是以组合来实现。
工作过程: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 最终所有的加载请求都会传送到顶层的启动类加载器中, 只有当父类加载器反馈自己无法完成这个请求时候, 才由子加载器来加载。
例如类Object
,它放在rt.jar
中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object
类在程序的各种类加载器环境中都是同一个类。
对于任何一个类, 都需要由加载它的类加载器和这个类本身一同确定其在 java 虚拟机中的唯一性。
ClassLoader.loadClass()
的代码如下,先检查是否已经被加载过,如果没有则parent.loadClass()
调用父加载器的loadClass()
方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException
,再调用自己的findClass()
方法进行加载。
另外,如果我们自己实现类加载器,一般是Override
复写 findClass
方法,而不是loadClass
方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //可以Override该方法
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
Java 后台的一点知识
JSP 与 Servlet 的关系
- Tomcat 等 Web 容器最终会把 JSP转化为 Servlet
- Jsp更擅长表现于页面显示, Servlet更擅长于逻辑控制
- Servlet是利用
System.out.println()
来输出 html 代码,由于包括大量的HTML标签、大量的静态文本及格式等,导致Servlet的开发效率低下 - JSP通过在标准的HTML页面中嵌入Java代码,其静态的部分无须Java程序控制,Java 代码只控制那些动态生成的信息
- 最终 JSP 被容器解释为 Servlet,其中Html 代码也是用
System.out.println()
等拼接输出的 - JSP 第一次访问的时候,要转化为 java 文件,然后编译为 class 文件,所以第一次访问 JSP 速度会比较慢,后面会快很多
Servlet 生命周期
主要是java.servlet.Servlet
接口中的init()
、service()
、和destroy()
3个方法。
- 初始化阶段,web容器通过调用
init()
方法来初始化Servlet
实例,在Servlet的整个生命周期类,init()
方法只被调用一次 - 客户请求到来时,容器会开始一个新线程,并调用servlet的
service()
方法,service()
方法根据请求的http方法来调用doget()
或dopost()
- 终止阶段调用
destroy()
方法,销毁一些资源
GET 请求 vs POST 请求
GET
用于信息获取,是安全的和幂等的,GET
一般是对后台数据库的信息进行查询POST
表示可能修改变服务器上的资源的请求,一般是对后台数据库进行增、删、改的操作GET
请求的参数会跟在URL后进行传递,请求的数据会附在URL之后,以?
分割URL和传输数据,参数之间以&
相连,一般浏览器对 URL 的长度会有限制POST
请求,提交的数据则放置在是HTTP包的包体中,用类似Key-Value
的格式发送一些数据,相对来说,GET
请求会把请求的参数暴露在 URL 中,安全性比POST
差一些
HTTP 请求的基本格式
- <
request line>
请求行 <headers>
请求头(参数头)<blank line>
空白行[<request-body>]
请求实体(GET没有, POST有
)
数据库
索引的分类
主要分为聚集索引和非聚集索引:
- 聚集索引存储记录物理上连续,而非聚集索引是逻辑上的连续,物理存储并不连续
- 聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个
ResultSet 统计记录数目
Java 中使用JDBC
连接数据库,最后都会得到一个 ResultSet,比如如下的代码
Connection con = DriverManager.getConnection(url, username, password);
Statement sta = con.createStatement();
String sql = "select * from student";
ResultSet resultSet = sta.executeQuery(sql);
那么如何根据得到的ResultSet
统计一共有多少条记录呢?注意:ResultSet
没有提供类似size()
、length
的 API 来直接获取总记录数。
方法1:利用循环
int sum = 0;
while(resultSet.next()){
sum++;
}
方法2:利用ResultSet的getRow方法来获得ResultSet的总行数
resultSet.last(); //移到最后一行
int rowCount = resultSet.getRow(); //得到当前行号,也就是记录数
设计模式
单例模式
单例模式中必须保证只有一个实例存在。有时候单例是为了避免重复创建多个实例造成资源浪费,有时候也是为了避免多个不同的实例导致系统不一致的行为。
Android 中,App启动时系统会创建一个Application
对象,用来存储系统的一些信息,这儿的Application
就是是单例模式的应用。可以通过Context.getApplicationContext()
获取唯一的Application
实例。
class Singleton {
private volatile static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (Singleton.class) {
//第二重判断
if (instance == null) {
instance = new Singleton(); //创建单例实例
}
}
}
return instance;
}
}
为什么synchronized
里面需要加一次判断if (instance == null)
,是考虑这样的特殊情形:比如线程A、B都到达第一个if (instance == null)
,线程A进入synchronized
代码中创建实例,线程B排队等待。但当A执行完毕时,线程B进入synchronized
锁定代码,它并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象。
也可以用内部类的方式创建,
public class Singleton(){
private static class Inner {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance(){
return Inner.instance;
}
}
模板方法模式
在父类中实现一个算法不变的部分,并将可变的行为留给子类来实现。
比如AsyncTask
里面的四个方法onPreExecute
、doInBackground
、onProgressUpdate
、onPostExecute
还有Activity
也应用了模板方法模式onCreate
、onStart
、onResume
、onPause
、onStop
、onDestroy
、onRestart
适配器模式
分为两种:类的适配器模式、对象的适配器模式
Android 里的 ListView
和 RecyclerView
的setAdapter()
方法就是使用了适配器模式。
观察者模式
在 GUI 中,不管是 Windows 桌面应用、或者 Android、IOS,都会给某个按钮 Button 设置监听事件,这儿就是使用了观察者模式。Android 中设置 Button 的监听事件代码如下:
final Button button = (Button) findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Perform action on click
}
});
笔试编程题
线程 VS 进程
关于线程和进程,不正确的描述是__。(选 D 栈是线程私有, 保存其运行状态和局部变量 )
A. 进程的隔离性要好于线程
B. 线程在资源消耗上通常要比进程轻量
C. 不同进程间不会共享逻辑地址空间
D. 同一个进程的线程之间共享内存,包括堆和栈
E. 进程间有途径共享大量内存中的数据
F. 线程间通讯可以通过直接访问全局变量,或者使用进程间通讯的机制(IPC)
找出未打卡的员工
题目:输入两行数据,第一行为全部员工的 id,第二行为某一天打卡的员工 id,已知只有一个员工没有打卡,求出未打卡员工的 id。(员工 id 不重复,每行输入的 id 未排序)
输入:
1001 1003 1002 1005 1004
1002 1003 1001 1004
输出:
1005
分析:可以用两个 List,第一个 List 保存所有员工的 id,第二个 List 保存打卡员工的 id,从第一个List 中把第二个 List 的数据都删除,最终剩下的就是未打卡员工的 id。
更好的方法:异或,两行数据中未打卡员工的 id 出现了一次,其余员工的 id 都出现了2次,两个相同的数异或为0。
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String[] ids = scan.nextLine().split(" ");
String[] marks = scan.nextLine().split(" ");
int result = 0;
for (int i = 0; i < ids.length; i++) {
result ^= Integer.parseInt(ids[i]);
}
for (int i = 0; i < marks.length; i++) {
result ^= Integer.parseInt(marks[i]);
}
System.out.println(result);
}
}
手写代码题
快速排序
排序是经典面试题,公司也希望通过手写快排来考察面试者的编程习惯和基本功。
// 排序范围 [start, end], 包含 end
public void sort(int[] arr, int start, int end) {
if (start < end) {
int p = partition(arr, start, end);
quickSort(arr, start, p - 1);
quickSort(arr, p + 1, end);
}
}
// 一次划分代码,返回被划分后的基准位置
public static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
while (left < right) {
while (left < right && arr[right] >= pivot)
right--;
if (left < right)
arr[left++] = arr[right];
while (left < right && arr[left] <= pivot)
left++;
if (left < right)
arr[right--] = arr[left];
}
arr[left] = pivot;
return left;
}
Note:快排是不稳定的,常见的稳定排序是:冒泡、插入、归并
括号字符串是否合法
某个字符串只包括(
和)
,判断其中的括号是否匹配正确,比如(()())
正确,((())()
错误,不允许使用栈
。
这种类似题的常见思路是栈,对于左括号入栈,如果遇到右括号,判断此时栈顶是不是左括号,是则将其出栈,不是则该括号序列不合法。
面试官要求不能使用栈,可以使用计数器,利用int count
字段。
public static boolean checkBrackets(String str) {
char[] cs = str.toCharArray();
int count = 0;
for (int i = 0; i < cs.length; i++) {
if (cs[i] == '(')
count++;
else {
count--;
if (count < 0) {
return false;
}
}
}
return count == 0;
}
扑克牌随机发牌
对于52张牌,实现一个随机打算扑克牌顺序的程序。52张牌使用 int
数组模拟。
该算法的难点是如何保证随机性?有个经典算法shuffle
,思路就是遍历数组,在剩下的元素里再随机取一个元素,然后再在剩下的元素里再随机取一个元素。每次取完元素后,我们就不会让这个元素参与下一次的选取。
To shuffle an array a of n elements (indices 0..n-1):
for i from n − 1 downto 1 do
j ← random integer with 0 ≤ j ≤ i
exchange a[j] and a[i]
注意这儿是0 ≤ j ≤ i
,包括j=i
的情况,因为可能洗牌后某个牌未发生交换,比如第51张牌还是原来的第51张牌。
public void randomCards() {
int[] data = new int[52];
Random random= new Random();
for (int i = 0; i < data.length; i++)
data[i] = i;
for (int i = data.length - 1; i > 0; i--) {
int temp = random.nextInt(i+1); //产生 [0,i] 之间的随机数
swap(data,i,temp);
}
}
摘自:http://www.codeceo.com/article/it-interview-question-2017.html#0-sqq-1-47287-9737f6f9e09dfaf5d3fd14d775bfee85