java(持续更新中)
java学习
1. 面向过程
1. 自动类型转换
-
低精度的数据类型会进行自动类型转换成高精度类型
byte b1 =10;//正确,尽管这是 int -> byte int n = 10; byte b2 = n;//错误,n 是一个 int 类型的变量,需进行强制类型转换当把具体的数赋值给 byte 时,先判断是否在 byte 范围之内(-128~127),如果在此范围即可转换
-
浮点数的默认为 double 类型
int n= 10; float f = n+1.1;//错误 float f = n+1.1F;//正确 -
byte 与 short 类型不会和 char 类型之间相互自动转换
-
byte,short,char 可以保存 int 的常量值(需要先判断范围是否符合),但是不能保存 int 变量值
-
byte ,short 与 char 三者之间可以进行运算,计算时首先转换为 int 类型
复合赋值运算符会自动进行强制类型转换,例如+=,-=,*=,/=,%=,++,--
byte b1 = 1; byte b2 = 2; byte b3 = b1 + b2;//错误,运算时转换成 int 类型了 byte b4 = 2;b4 +=2;//正确,复合赋值运算符会自动进行强制类型转换。例如+=,-=,*=,/=,%=,++,-- short s = 1; char c = 1;//int 类型的常量值可以赋给 char int n = b1 + b2 + s + c;//正确 -
强制类型转换可能会造成精度损失或者数据溢出
2. 基本数据类型与String类型的相互转换
-
基本数据类型转String类
将基本数据的值+“”即可
-
String类转基本数据类型(要先确保能转换成有效数据)
String s = "100"; int num = Integer.parseInt(s);
3. 原码,反码,补码
- 二进制的最高位是符号位,0表示正,1表示负(0->0,1->-)
- java没有无符号位
- 正数的原码,反码,补码都一样
- 负数的反码=原码符号位不变,其它位取反
- 负数的补码=反码+1(补码可以将正负数统一起来)
- 0的反码补码都是0
- 计算机运算时都是以补码方式来运算,看运算结果时要看原码
2. 面向对象
1.访问修饰符
| 访问级别 | 访问控制修饰符 | 同类 | 同包 | 子类 | 不同包 |
|---|---|---|---|---|---|
| 公开 | public | √ | √ | √ | √ |
| 受保护 | protected | √ | √ | √ | × |
| 默认 | 无 | √ | √ | × | × |
| 私有 | private | √ | × | × | × |
2. 继承(单继承机制)
-
子类继承的方法不能缩小访问权限;
-
子类继承了父类所有的属性和方法,非私有的属性和方法可以直接访问,但是私有的属性和方法不能在子类中直接访问,必须通过公共的方法去访问;
-
子类必须调用父类的构造器完成父类的初始化;
-
当创建子类的对象时,不管使用子类的哪个构造器,默认情况下总会调用父类的无参构造器super(),如果父类没有提供无参构造器,则必须在子类构造器中用super()去指定使用父类的哪个构造器完成对父类的初始化,否则编译不会通过;
-
如果子类的构造器希望指定调用父类的某个构造器,则显式调用super(参数列表);且必须放在构造器第一行,因为this(参数列表)也得放在第一行,所以这两个不能同时放在一个构造器中;
-
super与this的比较表
区别 this super 访问属性 访问本类中的属性,如果本类没有则从父类中继续查找,本类有则不会继续在父类中查找 访问父类中的属性,父类没有就从父类的父类中查找,直到顶级父类 调用方法 访问本类中的方法,如果本类没有则从父类中继续查找,本类有则不会继续在父类中查找 访问父类中的方法,父类没有就从父类的父类中查找,直到顶级父类 调用构造器 调用本类构造器,必须放在构造器的第一行 调用父类构造器,必须放在子类构造器的第一行 特殊 表示当前对象 子类中访问父类的对象
3. 多态
基础:
- 一个对象的编译类型和运行类型可以不一致(也可以一致)
- 编译类型在定义对象时就确定了,不能改变
- 运行类型是可以变化的
- 编译类型看 = 的左边,运行类型看 = 的右边
- 特点:可以调用父类中的所有可以访问的成员,不可以调用子类中特有的成员
难点:
- 动态绑定机制。当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定;当调用对象的属性时,没有动态绑定机制,哪里声明就哪里使用。
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum());//40,若将B中的sum()去掉,则答案30
System.out.println(a.sum1());//30,若将B中的sum1()去掉,则答案20
}
class A{
public int i = 10;
public int getI(){
return i;
}
public int sum(){
return getI() + 10;
}
public int sum1(){
return i + 10;
}
}
class B extends A{
public int i = 20;
public int getI(){
return i;
}
public int sum(){
return i + 20;
}
public int sum1(){
return i + 10;
}
}
4. 代码块(本质是对构造器的补充)
- static代码块也称为静态代码块,作用是对类初始化,随着类的加载而执行,且只会执行一次;
- 普通代码块每创建一个对象就会执行(与类加载与否无关);
- 类什么时候被加载(也就是静态代码块什么时候执行)?
- 创建对象实例时(new)
- 创建子类对象实例,父类也会被加载
- 使用类的静态成员(静态属性或静态方法),此时普通代码块不会被执行
- 创建对象时一个类调用的顺序如下
- 调用静态代码块和静态属性初始化(优先级一样,所以若有多个静态代码块和多个静态属性初始化,按照定义顺序调用)
- 调用普通代码块和普通属性初始化(优先级一样,所以若有多个普通代码块和多个普通属性初始化,按照定义顺序调用)
- 调用构造方法
- 构造器的最前面隐含了super()和调用普通代码块、普通属性初始化
- 静态相关的代码块和属性初始化在类加载就执行(静态最先执行),优先于普通代码块、普通属性初始化和构造器
总结以上,创建一个子类对象时调用关系如下:
- 父类的静态代码块和静态属性初始化(优先级一样,按照定义的顺序执行);
- 子类的静态代码块和静态属性初始化(优先级一样,按照定义的顺序执行);
- 父类的普通代码块和普通属性初始化(优先级一样,按照定义的顺序执行);
- 父类的构造方法;
- 子类的普通代码块和普通属性初始化(优先级一样,按照定义的顺序执行);
- 子类的构造方法。
public static void main(String[] args) {
B b = new B();
// System.out.println(B.sA);//不会调用B的静态相关的
}
class A {
public int a = setA();
public static int sA = setst();
public A() {
System.out.println("A Construct");
}
{
System.out.println("A普通代码块执行");
}
static {
System.out.println("A静态代码块执行");
}
public int setA() {
System.out.println("A普通属性初始化");
return 10;
}
public static int setsA() {
System.out.println("A静态属性初始化");
return 50;
}
}
class B extends A {
public int s = sets();
{
System.out.println("B普通代码块执行");
}
static {
System.out.println("B静态代码块执行");
}
public B() {
System.out.println("B Construct");
}
public int sets() {
System.out.println("B普通属性初始化");
return 10;
}
}
5.单例设计模式
饿汉式VS懒汉式:
- 最主要的区别时创建对象时机不同:饿汉式在类加载就创建对象实例,懒汉式在使用时才创建;
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题;
- 饿汉式存在浪费资源问题,懒汉式不存在浪费资源问题;
6.final
-
final修饰的属性是常量,一般用大写字母命名(XX_XX_XX);
-
final修饰的属性在定义时,必须赋初值,且以后不可修改;
-
final修饰的普通属性赋初值的位置如下:1.定义时;2.在构造器中,3.代码块中;
-
final修饰的静态属性赋初值的位置如下:1.定义时;2.静态代码块中(不可以在构造器中);
-
final类不能继承,但是可以实例化对象;
-
如果不是final类,但是还有final方法,则该方法虽然不可以被重写,但是可以被子类继承;
-
如果一个类是final类,则其方法没必要用final修饰;
-
final不能修饰构造器;
-
final和static搭配效率更高,因为不会导致类的加载
public static void main(String[] args) { System.out.println(D.a);//只会输出100 } class D{ public static final int a=100; { System.out.println("普通代码块");//创建对象时执行; } static { System.out.println("静态代码块");//类加载时执行,未执行说明类未加载; } }
7.抽象类
- abstract只能修饰类和方法,不可用来修饰属性和其它的;
- 抽象类不能实例化对象;
- 抽象类可以没有abstract方法(抽象类可以有任意的成员),有抽象方法一定要声明为abstract类;
- 抽象方法不可以用private,final,static修饰,因为这些关键字与方法重写相违背;
- 抽象方法:访问修饰符 abstract 返回值类型 方法名(),没有方法体;
8.接口
- 接口不能实例化对象;
- 接口中的所有方法都是public(默认不是default)和abstract(可以省略不写);
- 一个类只能直接继承一个父类,但是可以实现多个接口;
- 接口中的属性只能是public final static修饰的(
即int a = 10;等价public final static int a = 10;),因为是final,所以必须初始化属性; - 接口不能继承别的类,但是可以继承(extends)多个接口;
- 一个类可以实现(implements)多个接口;
- JDK1.8开始可以在接口定义普通方法(必须default声明,调用方法:接口对象.函数名)和静态方法(必须static声明,调用方法:接口名.函数名)。这些方法都会被实现接口的类继承。
9.接口与继承的对比
继承的目的:解决代码的复用性和可维护性,只能单继承,是is-a的关系;
接口的目的:设计好各种方法,让其他类去实现,更加灵活,可以实现多个接口,是like-a的关系;
10.内部类
内部类分类:
定义在外部类局部位置上(比如方法中):
1.局部内部类(有类名)
2.匿名内部类(无类名)
定义在外部类成员位置:
1.成员内部类(无static修饰)
2.静态内部类(有static修饰)
1. 局部内部类:
- 可以直接访问外部类的所有成员,包含私有的
- 外部类访问局部内部类成员需要先创建对象再访问,且需要在作用域内
- 不能添加访问修饰符修饰局部内部类,但是可以添加final修饰
- 作用域:仅仅在定义的方法或者代码块中
- 外部类和局部内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名
2. 匿名内部类:(重要)
-
可以直接访问外部类的所有成员,包含私有的
-
匿名内部类既是一个类,同时本身也为一个对象,所以可以作为参数
-
外部类和匿名内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名
-
匿名内部类用来简化开发:当接口或者类的某个方法只需要使用一次时,无需创建一个类来implement接口了
public static void main(String[] args) { new A(){ @Override public void nice() { System.out.println("简化开发");//外部类名$1 } }.nice(); A a = new A(){ @Override public void nice() { System.out.println("简化开发");//外部类名$2 } }; a.nice(); System.out.println(a.getClass()); } interface A { int n = 200; void nice(); } -
匿名内部类的getClass()按照在外部类中的定义顺序依次为“外部类名$+序号" 序号=1,2,3……
-
语法:
new class or interface(填写参数){
类体(方法重写)
};
3. 成员内部类:
-
可以直接访问外部类的所有成员,包含私有的
-
外部类访问成员内部类成员需要先创建对象再访问,且可以访问成员内部类的私有成员
-
外部其它类访问成员内部类 (前提是满足访问权限)
Outer2 outer = new Outer2();Outer2.Inner InnerInstance1 = outer.new Inner();Outer2.Inner InnerInstance2 = outer.getInnerInstance();
-
可以添加任何修饰符修饰该类,因为它就是成员
-
作用域:作用于整个类体
-
外部类和成员内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名
4. 静态内部类:(static修饰)
- 可以直接访问外部类的所有静态成员,包含私有的
- 外部类访问静态内部类成员需要先创建对象再访问,且可以访问成员内部类的私有成员
- 外部其它类访问静态内部类 (前提是满足访问权限),方法同成员内部类中
- 可以添加任何修饰符修饰该类,因为它就是成员
- 外部类和静态内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.成员名
11.String类(StringBuffer,StringBuilder)

- intern()返回的是常量池中的地址,new出来的String返回的是堆中的地址
- String两个常量相加看的是常量池;两个变量相加看的是堆,String常量+变量(变量+常量)看的也是堆,而且一定是new出来的
String a = "a";
String b = "b";
String sum1 = a + b;//创建了3个对象
String sum2 = "a" + "b";//创建一个对象,编译器会优化,等价于String sum2 = "ab";
System.out.println(sum1 == sum2);//false
public static void main(String[] args) {
//验证String的不可变
String s = "hello";
String02 string02 = new String02();
string02.changeString(s);//非静态方法不可直接调用,需要通过对象调用
System.out.println(s);
StringBuffer sb = new StringBuffer("hello");
changeStringBuffer(sb);
System.out.println(sb);
}
public void changeString(String s) {
s = s + "world";
}
public static void changeStringBuffer(StringBuffer sb) {
sb.append("world");
}
-
String保存的字符串常量,里面的值不可修改(public final char value[]),每次String类更新实际是更改地址,效率低
-
StringBuffer保存的字符串变量,里面的值可修改(char value[],存放在堆中),每次StringBuffer类更新实际是更改内容,不更新地址(只有空间不够才更新地址),效率高
-
StringBuilder的方法没有互斥处理(没有synchronized关键字),因此在单线程情况下应用
-
三者比较
String StringBuffer StringBuilder 序列可变性 不可变字符序列 可变字符序列 可变字符序列 效率 低 较高(增删) 最高 优点 复用率高(常量池) 线程安全
synchronized关键字线程不安全 缺点 对字符串大量修改时
不用String -
String 、 StringBuffer 和 StringBuilder 的选择使用的原则,结论:
- 如果字符串存在大量的修改操作,一般使用 StringBuffer 或 StringBuilder
- 如果字符串存在大量的修改操作,并在单线程的情况,使用 StringBuilder
- 如果字符串存在大量的修改操作,并在多线程的情况,使用 StringBuffer
- 如果我们字符串很少修改,被多个对象引用,使用 String ,比如配置信息等
- StringBuilder 的方法使用和 StringBuffer 一样,不再说
long StartTime = 0L, EndTime = 0L;
StringBuilder stringBuilder = new StringBuilder("");
StartTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
stringBuilder.append(String.valueOf(i));
}
EndTime = System.currentTimeMillis();
System.out.println("StringBuilder运行时间="+(EndTime - StartTime));
StringBuffer stringBuffer = new StringBuffer("");
StartTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
stringBuffer.append(String.valueOf(i));
}
EndTime = System.currentTimeMillis();
System.out.println("StringBuffer运行时间="+(EndTime - StartTime));
String s = "";
StartTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
s += i;
}
EndTime = System.currentTimeMillis();
System.out.println("String运行时间="+(EndTime - StartTime));
12.Collection

List
1.ArrayList
-
ArrayList 中维护了一个 Object 类型的数组 elementData(transient Object[] elementData; transient瞬间、短暂的,表示该属性不会被序列化)
-
当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为0,第1次添加,则扩容 elementData 为10,如需要再次扩容,则扩容 elementData 为1.5倍(即:size + size/2)
-
如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为1.5倍
-
没有synchronized关键字,线程不安全
-
ArrayList和Vector比较
底层结构 线程和效率 扩容 ArrayList transient Object[] elementData 不安全,效率高 有参时扩容每次1.5倍
无参数时第一次扩容10,以后1.5倍Vector protected Object[] elementData 安全,效率不高 有参时扩容每次2倍
无参数时第一次扩容10,以后2倍
2.LinkedList
- 线程不安全,增删效率高,改查效率低
Set:
- Set接口无序(添加和取出顺序不一致,每次取出的顺序固定),没有索引(所有不能使用普通for循环遍历),不允许重复元素(所以最多一个null)
1.HashSet(底层结构:数组+链表+红黑树)
添加元素机制:
-
先获取元素的哈希值hash( hashCode 方法,hash值≠hashCode)
-
对哈希值进行运算,得出一个索引值即为要存放在哈希表(
transient Node<K,V>[] table;)中的位置号 -
如果该位置上没有其他元素,则直接存放;
如果该位置上已经有其他元素,则需要进行 equals 判断:
如果相等,则不再添加:
Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;如果不相等则以链表的方式添加:
else { for (int binCount = 0; ; ++binCount) {//若table对应的索引位置是链表,依次在链表中比较 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//添加完元素后判断该链表是否有8节点,达到则在treeifyBin中判断该链表是否要树化 break;//添加元素成功 } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break;//如果存在相同元素,则退出循环,添加失败 p = e;//就是=p.next } }附:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//添加失败 } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } // final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
扩容机制:
- HashSet 底层是 HashMap(transient Node<K,V>[] table;),初始化若调用默认构造器,第一次添加时,table数组扩容到16,临界值(threshold)是16×加载因子(loadFactor是0.75)=12;若调用带参数构造器,则数组容量为≥initialCapacity的且能整除4的最小整数(未add元素之前,无论初始化构造器是否有参数,table=null)
- 如果 table 数组使用大于临界值12(每加入一个节点,table数组size++,不论是加在第一个还是链表中,当size>12就会扩容),就会扩容到16*2=32,新的临界值就是32×0.75=24,依次类推
- Java8中,如果一条链表的元素个数到达 TREEIFY _ THRESHOLD(默认是8)并且 tabe 的大小>=MINTREEIFY _ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
2.LinkedHashSet(底层结构:数组+双向链表)
-
在 LinkedHastSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有 head 和 tail)
-
每一个节点有 before 和 after 属性,这样可以形成双向链表
-
在添加一个元素时,先获取元素的哈希值( hashCode 方法),再求索引,确定该元素在 table 的位置,然后将添加的元素加入到双向链表(如果已经存在,不再添加,添加元素机制原则和 hashset 一样)
tail.next = newElement newElement.pre = tail tail = newEelment ; -
这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致
13.Map

- Map 与 Collection 并列存在。用于保存具有映射关系的数据: Key - Value
- Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
- Map 中的 key 不允许重复,value 可以重复,原因和 HashSet 一样,前面分析过源码
- Map 的 key 可以为 nul , value 也可以为 null ,注意 key 为 null ,只能有一个, value 为 null ,可以多个。
- 常用 String 类作为 Map 的 key
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value,get(key)方法
HashMap:(基本与HashSet一致)

Hashtable:
- 与 HashMap 不同,Hashtable的 key 和 value 都不能为 null
- 底层 table 为 Hashtable$Entry[](
private transient Entry<?,?>[] table;),初始大小11,数组扩容每次(×2+1)

浙公网安备 33010602011771号