一、Java:按值传递还是按引用传递详细解说
- java只有值传递
- 基本类型,直接按值进行传递,所传的值为原值的副本,不改变;
- 引用类型指向一个实例对象,p->persion,所传的值也是p的副本(-p),当改变-p指向的对象时,对象本身即发生改变。p指向并没有被影响到。这两个其实是一个意思,说多了反而复杂了。
- 至于String对象, 有一个很好地解释:
String传递的也是引用副本的传递,但是因为String为final的,所以和按值传递等同的
二、String常量池
- 常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。java会在将一些通过String s = “java”这种申明方式所产生的值保存再常量池中,当再次采用这种方式赋值时,不会new 新的对象,而是直接在常量池中取用。
String s = "hello";
String s2 = "hello";
System.out.println(s == s2);// 输出为true
三、Java中堆内存和栈内存详解
- 栈:栈中存放的是变量,不能称之为对象。在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
- 堆:堆内存用于存放由new创建的对象和数组。
四、反转链表
- 反转链表-循环
采用双指针,主要是4行代码,其中2,3俩行完成指针反转,1,4主要是保持head往下指
public Node reverseList(Node head) {
// 安全性检查
if (head == null || head.next == null)
return head;
Node pre = null;
Node temp = null;
while (head != null) {
// 以下1234均指以下四行代码
temp = head.next;// 与第4行对应完成头结点移动
head.next = pre;// 与第3行对应完成反转
pre = head;// 与第2行对应完成反转
head = temp;// 与第1行对应完成头结点移动
}
return pre;
- 反转链表-递归
public static Node reverseListRec(Node head) {
if (head == null || head.next == null)
return head;
Node reHead = reverseListRec(head.next);
head.next.next = head;
head.next = null;
return reHead;
五、split 点号 split(“.”)
- 对于下面的一段代码:在使用 Java split() 方法时,希望把版本号中的数字组成数组。很自然的,我用了 split(“.”) 来分割成数组,结果不行。
String v = "1.0.1";
String[] vs = v.split(".");
int len = vs.length;
for(int i = 0; i<len; i ++){
System.out.println(vs[i]);
}
查 API,得知 split 的参数是 String regex 代表的是一个正则表达式。如果是正则中的特殊字符,就不能了。点正好是一个特殊字符。如下方式解决:
String v = "1.0.1";
String[] vs = v.split("[.]");
int len = vs.length;
for(int i = 0; i<len; i ++){
System.out.println(vs[i]);
}
六、从源代码看HashMap的加载因子和容量分配
- HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是capacity=16,加载因子是load_factor=0.75。
- 容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
七、Hashmap为什么容量是2的幂次
- h & (length - 1) == h % length,当length是2的幂次时,这个算式才成立。由于位运算比取余运算快捷,所以java内部,采用这个方式加快运算效率。
- 这里h是通过K的hashCode最终计算出来的哈希值,并不是hashCode本身,而是在hashCode之上又经过一层运算的hash值,length是目前容量。这块的处理很有玄机,与容量一定为2的幂环环相扣,当容量一定是2^n时,h & (length - 1) == h % length,它俩是等价不等效的,位运算效率非常高,实际开发中,很多的数值运算以及逻辑判断都可以转换成位运算,但是位运算通常是难以理解的,因为其本身就是给电脑运算的,运算的是二进制,而不是给人类运算的,人类运算的是十进制,这也是位运算在普遍的开发者中间不太流行的原因(门槛太高)。
- h & (length - 1) == h % length证明。例如:
length = 1000 的时候,
length -1 = 0111,
L 和 length 二进制位数相同,只要和 (length -1)进行“&”的运算,得到的就是取模的结果。这主要是因为 length 是 2^n ,例如 n = 3 的时候,1000 就是最大的数字,而减掉 1 后,恰恰是 0,1 互相翻转,变成了 0111,所以用位运算可以处理。如果不是 2^n,就不行了。
Hashmap为什么容量是2的幂次,什么是负载因子
当length为2^n, m & (length-1) 相当于 m % length 的证明
八、《Thinking in Java》习题——吸血鬼数字高效版
- 找出四位数的所有吸血鬼数字
吸血鬼数字是指位数为偶数的数字,可以由一对数字相乘而得到,而这对数字各包含乘积的一半位数的数字,其中从最初的数字中选取的数字可以任意排序.
- 以两个0结尾的数字是不允许的。
- 例如下列数字都是吸血鬼数字
1260=21*60
1827=21*87
2187=27*81
…
import java.util.Arrays;
/**
* 吸血鬼数字,高效率版本.<br>
* 一个4位数字,可以拆分2个2位数数字的乘积,顺序不限。<br>
* 比如 1395 =15 * 93
*
* @author 老紫竹(laozizhu.com)
*/
public class Vampire {
public static void main(String[] arg) {
String[] ar_str1, ar_str2;
int sum = 0;
int from;
int to;
int i_val;
int count = 0;
// 双重循环穷举
for (int i = 10; i < 100; i++) {
// j=i+1避免重复
from = Math.max(1000 / i, i + 1);
to = Math.min(10000 / i, 100);
for (int j = from; j < to; j++) {
i_val = i * j;
// 下面的这个代码,我个人并不知道为什么,汗颜
if (i_val % 100 == 0 || (i_val - i - j) % 9 != 0) {
continue;
}
count++;
ar_str1 = String.valueOf(i_val).split("");
ar_str2 = (String.valueOf(i) + String.valueOf(j)).split("");
Arrays.sort(ar_str1);
Arrays.sort(ar_str2);
if (Arrays.equals(ar_str1, ar_str2)) {// 排序后比较,为真则找到一组
sum++;
System.out.println("第" + sum + "组: " + i + "*" + j + "=" + i_val);
}
}
}
System.out.println("共找到" + sum + "组吸血鬼数");
System.out.println(count);
}
}
可以看到,只比较了232次,如果普通的大致有4000次,其中的关键部分
if (i_val % 100 == 0 || (i_val - i - j) % 9 != 0) {
continue;
}
关于算法的解释,来自网友MT502
假设val = 1000a + 100b + 10c + d, 因为满足val = x * y, 则有x = 10a + b, y = 10c + d
则val - x - y = 990a + 99b + 9c = 9 * (110a + 11b + c), 所以val - x - y能被9整除。
所以满足该条件的数字必定能被9整除,所以可以直接过滤其他数字。
我准许做一下
x*y = val = 1000a + 100b + 10c + d;
我们假设
x = 10a + b, y = 10c + d
则
x*y-x-y
= val - x-y
= (1000a + 100b + 10c + d) - (10a+b) - (10c +d) = 990a + 99b + 9c
= 9 * (110a + 11b + c);
对于别的组合可能性,结果一样,比如
x=10c+a; y=10d+b;
x*y-x-y
= val - x-y
= (1000a + 100b + 10c + d) - (10c+a) - (10d +b) = 999a + 99b -9d
= 9 * (110a + 11b -d);
当然也能被9整除了
《Thinking in Java》官方答案
其中:if((num1 * num2) % 9 != (num1 + num2) % 9)等效于(i_val - i - j) % 9 != 0
九、java中的四种引用
- 强引用:直接创建的对象,我们使用的大部分引用实际上都是强引用,如String s=”hello”;只要存在引用指向关系,就不会被回收。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止。
- 软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用:它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
Java的四种引用,强弱软虚,用到的场景
十、从一个栈引出的内存泄露问题
- 我记得在有一次面试中,面试官问我自己实现的一个栈中会不会有内存泄露的问题,我努力搜索可能的问题,就是感受不到可能出现的问题。当时忽然意识到,内存泄露这个问题一直被我忽略,因为用的是java/C#,这些语言中都有内存自动回收的机制,我突然发现自己对这个问题竟然一无所知。面试中的栈就是下面这个:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 保证栈能自动增长,当栈中空间不足时,自动增长为原长度的两倍
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
。定位到pop()函数,在return语句中,当我们弹出一个元素时,只是简单的让栈顶指针(size)-1。逻辑上,栈中的这个元素已经弹出,已经没有用了。但是事实上,被弹出的元素依然存在于elements数组中,它依然被elements数组所引用,GC是无法回收被引用着的对象的。也许你期望等这整个栈失去引用(将被GC回收时),栈内的elements数组一起被GC回收。但是实际的使用过程中,又有谁能够预料到这个栈会存活多长时间。为了保险起见,我们需要在弹出一个元素的时候,就让这个元素失去引用,便于GC回收。我们只需要让Pop()函数弹出时,同时解除对弹出元素的引用即可。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 消除过期的引用
return result;
}
十一、 为什么类必须实现接口的所有方法
- 这个问题应该好好看看接口的定义:对于实现了我的所有类,看起来都应该象我现在这个样子!!
- 比如实现接口Car的对象都可以run(), 你就必须要提供run()方法。
你要是不提供,那就不是Car接口的实现了 - 因此,采用了一个特定接口的所有代码都知道对于那个接口可能会调用什么方法.这便是接口的全
部含义.所以我们常把接口用于建立类和类之间的一个“协议”。 - 我的理解:接口就是一个功能的整体,由于接口自身不能实例化,所以实现此接口的对象任何功能都不能缺少,否则就不能够完全拥有此功能。具体的看下一个编译期和运行期,以及多态的深入理解。
十二、编译期和运行)-List list = new ArrayList();和ArrayList list=new ArrayList();的区别
- 编译期:list是一个list对象,只能调用本身接口中的方法,在程序未运行时list只能当做List类型;(多态)
运行期:list是一个ArrayList对象,在程序运行时JVM会把list当做ArrayList类型 List是一个接口,而ArrayList 是一个类。 ArrayList 继承并实现了List。
List list = new ArrayList();这句创建了一个ArrayList的对象后把上溯到了List。此时它是一个List对象了,有些ArrayList有但是List没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了ArrayList的所有属性。为什么一般都使用 List list = new ArrayList() ,而不用 ArrayList alist = new ArrayList()呢?
- 问题就在于List有多个实现类,如 LinkedList或者Vector等等,现在你用的是ArrayList,也许哪一天你需要换成其它的实现类呢?,这时你只要改变这一行就行了:List list = new LinkedList(); 其它使用了list地方的代码根本不需要改动。假设你开始用 ArrayList alist = new ArrayList(), 这下你有的改了,特别是如果你使用了 ArrayList特有的方法和属性。 ,如果没有特别需求的话,最好使用List list = new LinkedList(); ,便于程序代码的重构. 这就是面向接口编程的好处。
十三、深入理解多态
- 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
理解java的三大特性之多态