Java基础
1、Integer缓存池
- Integer缓存池范围:-128-127 1个字节
- Integer.valueOf()会使用缓存池中的对象,多次调用会获得同一个对象的引用。
- 编译器会在自动装箱的过程调用valueOf()方法,因此在缓存值范围内的使用自动装箱来创建的话,会引用相同的对象。
Integer m = 123;
Integer m = 123;
System.out.println(m==n); //true
2、字符串常量池
- 字符串常量池保存着所有的字符串字面量,这些字面量在编译的时候就确定了。
- String的intern()方法在运行的时候将字符串添加到字符串常量池中。
- 当一个字符串调用intern()方法的时候,如果String pool中已经存在了一个字符串和该字符串的值相等,那么就会返回String pool中字符串的引用,否则就会在String pool中添加一个新的字符串,并返回新的字符串的引用。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1==s2); //false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3==s4); //true
Java1.7之前String pool被放在运行时常亮池中,属于永久代(方法区中),在Java7,String Pool被移到了对堆里面,这是因为永久代的空间有限,在大量使用字符串的时候会出现OOM错误。
直接反编译String s = “abc”;
public class Test
{
public static void main(String[] args)
{
String s1 = "abc";
}
}
new String(“abc”)会创建几个对象?
一个或者两个(主要就是看常量池中的字面量创建还是不创建)。
- new会创建一个字符串对象。
- 还有一个就是常亮池中创建还是不创建这个字面量。
public class Test
{
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
}
}
ldc的含义是:ldc系列命令负责把数值常量或String常量值从常量池中推送至栈顶。从上图中,我们可以看到第0行和第7行中的字符串引用是同一个,这说明了,在编译期间,该字符串变量的值已经确定了下来,并且将该字符串值缓存在缓冲区中,同时让该变量指向该字符串值,后面如果有使用相同的字符串值,则继续指向同一个字符串值。
- JVM的new指令会创建一个对象并把引用入栈。
- JVM的dup指令会复制栈顶数值,并且复制值进栈
- JVM的ldc指令会命令负责把数值常量或String常量值从常量池中推送至栈顶。
第9行调用String的String(String str)实例构造器,执行new String(字面量),需要使用操作数栈的最上面的两个元素(new指令得到的一个引用和ldc指令放入的数值常量),并把new String()的引用放到操作数栈中。
String ss = “abc”+“bcd”;会创建几个对象
public class Test
{
public static void main(String[] args)
{
String ss = "abc"+"bcd";
}
}
从代码显示就创建了一个对象
String ss = s1+s2;创建了几个对象?
public class Test
{
public static void main(String[] args)
{
String s1 = "abc";
String s2 = "bcd";
String ss = s1+s2;
}
}
创建了三个对象,分别是在常量池中创建了“abc”和“bcd”,还有一个是StringBuilder对象,两个String对象相加底层是调用StringBuilder来实现的。
3、参数传递
Java参数传递是以值传递的方式。
class Student
{
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Test
{
public static void changeStudent(Student student)
{
System.out.println(student);
student = new Student();
System.out.println(student);
}
public static void main(String[] args)
{
Student student = new Student();
System.out.println(student);
System.out.println("changeStudent---------");
changeStudent(student);
System.out.println("changeStudent---------");
System.out.println(student);
}
}
输出
Student@61bbe9ba
changeStudent---------
Student@61bbe9ba
Student@610455d6
changeStudent---------
Student@61bbe9ba
从输出可以看出,前后都是Student@61bbe9ba,没变,所以是传值。
本质是将地址以值的形式传递,因此在方法中使指针引用其他对象,那么这两个指针此时其他是完全不同的对象,一个改变对另外一个没影响。
4、向下转型(精度会降低。)
Java不能隐式向下转型,但是可以通过++和+=实现。
比如下面的例子就是错误的:
float f = 1.1; //错误
float f1 = 1.1f; //正确
short s1 = 1; //正确
s1 = s1 + 1; //错误
s1 += 1; //正确
s1++; //正确
5、switch(不支持long,Java7开始支持String)
支持byte、short、char、int、String、enum以及前面四个基本类型的包装类。
6、修饰符
访问修饰符 | 本类 | 本包 | 子类 | 其他 |
---|---|---|---|---|
public | true | true | true | true |
protected | true | true | true | false |
默认 | true | true | false | false |
private | true | false | false | false |
7、抽象类和接口
抽象类和普通类的最大的区别是:
抽象类不可以被实例化。
接口和抽象类区别:
- 接对于方法而言:接口的成员(字段和方法)必须是public的,不允许定义为private和protected,而抽象类的成员可以有多种访问限制。
- 对于字段而言:接口的字段默认是static和final的,抽象类的字段没有这种限制。
- 一个类可以实现多个接口,但是只能继承一个抽象类。
抽象类可以在几个相关的类中共享代码。而接口可以使得实现类都实现接口的方法。
8、重写和重载
为了满足里式替换原则。重写有三个限制:
- 1、子类方法的访问权限要大于等于父类
- 2、子类方法的返回值必须是父类方法返回类型或者是其子类
- 3、子类方法抛出的异常必须是父类抛出异常类型或者是其子类型。
重载:
- 重载不能参数类型相同,返回值不同
- 重载指的是一个方法与已经存在的方法函数名称相同,但是参数类型、个数、顺序至少有一个不同。
9、hashCode()和equals()方法
equals()相同的话,hashCode()必须相同。
因为如果不等的话,可能会破坏JDK一些类的特性,比如因为散列值不同,两个equals的对象可以放进Set中,使得Set会添加两个等价的对象,从而破坏Set的特性。
10、深拷贝和浅拷贝
首先做出回答:因为如果不继承自Cloneable接口,当调用clone()时会抛出CloneNotSupportedException异常,其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对 Object类中clone()方法的。
浅拷贝
拷贝对象和原始对象引用同一个对象。
深拷贝
拷贝对象和原始对象引用不同对象。
11、反射讲解
Java 中的反射首先是能够获取到 Java 中要反射类的字节码,获取字节码有三种方法:
- 1.Class.forName(className)
- 2.类名.class
- 3.this.getClass()。
然后将字节码中的方法,变量,构造函数等映射成相应的 Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
/**
* 取得全部普通方法:
*public Method[] getMethods() throws SecurityException
* 取得指定普通方法:
*public Method getMethod(String name, Class<?>... parameterTypes)
* 以上两个方法范辉的类型是java.lang.reflect.Method类的对象,在此类中提供有一个调用方法的支持:
* 调用:
*public Object invoke(Object obj, Object... args)throws
* IllegalAccessException, IllegalArgumentException,InvocationTargetException
*/
import java.lang.reflect.Method;
class Person
{
private String name;
private int age;
public Person(){}
public Person(String name,int age)
{
this.age = age;
this.name = name;
}
@Override
public String toString()
{
return "name is:"+this.name+" "+"age is:"+this.age;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
}
public class ClassTest
{
public static void main(String[] args) throws Exception
{
Class<?> classes = Person.class;// 任何时候调用类中的普通方法都必须有实例化对象
Method[] GetMethods = classes.getMethods();
Object object = classes.newInstance(); // 取得setName这个方法的实例化对象,设置方法名称与参数类型
// 随后需要通过Method类对象调用指定的方法,调用方法需要有实例化对象
// 同时传入参数
Method method = classes.getMethod("setName", String.class);
method.invoke(object,"Regina Spektor");// 相当于Person对象.setName("Regina Spektor") ;
Method method1 = classes.getMethod("getName");
Object result = method1.invoke(object);//相当于Person对象.getName()
System.out.println("My favourite singer is "+result);
}
}
12、静态方法可以是抽象的吗?
不可以,静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现。
方法中不可以有this和super关键字。
13、静态代码块块
```java
public class A
{
static
{
System.out.println("123");
}
public static void main(String[] args)
{
A a1 = new A();
A a2 = new A();
}
}
输出:
123
静态语句块在类初始化时运行一次。
14、静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。
public class OuterClass
{
class InnerClass
{
}
static class StaticInnerClass
{
}
public static void main(String[] args)
{
// InnerClass innerClass = new InnerClass();
// 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
15、Throwable
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
- 受检异常 :需要用 try…catch… 语句捕获并进行处理或者throws给调用者,并且可以从异常中恢复;一般是除了一般是RuntimeException及其子类之外的类。
- 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复,一般是RuntimeException及其子类。
# 容器
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射 表。
Java集合
Collection(继承了Iterator)
Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
Set、List、Queue三个子接口
Queue
- LinkedList:可以用它来实现双向队列。
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
Set
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说 使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
List
- ArrayList:基于动态数组实现,支持随机访问。
- Vector:和 ArrayList 类似,但它是线程安全的。
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。
List相关源码
ArrayList源码
- 默认大小为10。扩容为1.5倍扩容即oldCapacity+(oldCapacity>>1),也就是旧容量的1.5倍。
- 通过Arrays.copyOf()把原数组整个复制到新数组中。
Fail-Fast快速失败
modCount用来记录ArrayList结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组大小,仅仅设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作的时候,需要比较操作前后modCount是否改变,如果改变了需要抛出ConcurrentModificationException。
Vector源码
- 初始大小10,扩容2倍
- 方法同步
Collection.synchronizedList();实现现场安全的List
List list = new ArrayList();
List synList = Collections.synchronziedList(list);
CopyOnWriteArrayList(适合读多写少)
- 写操作在一个复制的数组上进行,读操作还是在原始数组上进行,这样实现读写分离,互不影响。
- 写操作时候会加锁,使用ReentrantLock加锁。
- 写操作的时候允许读操作,大大提高了读操作的性能,很适合读多写少的场景。
缺点
- 内存占用:写操作的时候要复制一个新的数组,使得内存占用为原来数组两倍左右。
- 数据不一致:读操作不能读取到实时的数据,因为部分写操作的数据还未同步到读数组中。
Map
- TreeMap:基于红黑树实现。
- HashMap
- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并 且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安 全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序
顺序由accessOrder决定,当为true的时候,表示LRU顺序,当一个节点被访问的时候,会将该节点移动到链表尾部,从而包装尾部是最近访问的节点,那么首部就是最近最久未使用的节点。
Map相关源码
HashMap
1.7的HashMap(Entry数组+链表)
底层上一个Entry数组即Entry<K,V>[] table。Entry是一个节点。Entry节点中包含key、value、hashCode以及Entry next四个字段。
数组的每个位置被当成一个桶。
- 使用数组和链表实现,使用拉链法解决冲突
- 链表以头插法的方式插入数据
- 大小必须是2的幂次方(因为2的幂次方减去1之后,后面都变成了1,再进行按位与和取模效果是一样的)。
- HashMap可以放入key为null的键值对,但是无法调用null的hashCode(根据key来计算的),也就无法计算桶下标,所以HashMap使用桶0存放键为null的键值对。
- 加载因子是0.75,根据泊松分布计算得来的。
- resize在并发的时候可能会产生死锁。
- 在resize的时候使用一种特殊的方式降低元素移动
比如:原来大小为16,扩容之后为32。他的哈希值如果在第5位上为0,则按位与之后结果和以前一样,如果为1,则得到的结果会加16。
1.8的HashMap
- 使用数组+链表+红黑树的数据结果。
- 从1.8开始,在一个桶存储的链表长度大于8的时候会将链表转换为红黑树。
- 插入使用的是尾插法。
1.7的ConcurrentHashMap
里面维护一个Segment数组。Segment节点里面有HashEntry数组。
static final class HashEntry<K,V>
{
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
Segment数据结构
static final class Segment extends ReentrantLock implements Serualizable
{
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors()>1 ? 64:1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
- 使用分段锁Segment,每个分段锁维护几个桶,多个线程可以同时访问不同分段锁上的桶,从而使得并发度更高(并发度就是Segment的个数),Segment继承自ReentrantLock
- 默认的并发级别是16即static final int DEFAULT_CONCURRENCY_LEVEL = 16;
- Segment维护一个count变量来统计Segment中键值对的个数。在计算size的时候要遍历所有的Segment把count加起来。
- ConcurrentHashMap在执行size的时候先尝试不加锁,如果连续两次不加锁操作得到的结果是一致的,那么这个结果就是正确的即RETRIES_BEFORE_LOCK为2,retries初始值为-1,因此尝试次数为3,在三次中如果连续两次得到结果一致,那么认为结果正确,否则加锁。
1.8的ConcurrentHashMap
- 1.7并发度和Segment数量相等。JDK1.8使用CAS来支持更高的并发度
- 在cas操作失败的时候,使用内置锁synchronized。
- JDK1.8的实现也在链表过长的时候转换为红黑树。