Java
文章目录
JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用的,能够创建和编译 Java 程序。他 包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)。。。
JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)
编译:一次性编译完成(java—class)Java编译器【idea、eclipse…】
解释:一句句解释(class—机器码)JVM
标识符:名字(变量名、类名。。。)
关键字:含特殊含义的标识符
8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:
byte
(-128~127)、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔型:
boolean
。
包装类型
Integer x = 2 //Integer,valueOf(2)
int y = x //x.intValue()
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。【valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。】
Integer i1 = 40;//缓存中拿的
Integer i2 = new Integer(40);//创建的新对象
System.out.println(i1==i2);//false
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
Java 不能隐式执行向下转型,因为这会使得精度降低。
// float f = 1.1
float f = 1.1f
String
字符型常量:char,占2个字节
字符串常量:String
String中的value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
好处:缓存 hash 值(String 的 hash 值经常被使用)、String Pool的需要(如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用)、线程安全(可以在多个线程中安全地使用)
- String 不可变,因此是线程安全的
- StringBuilder可变, 不是线程安全的
- StringBuffer可变, 是线程安全的,内部使用 synchronized 进行同步
典中典:
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
new String(“xxx”) 使用这种方式一共会创建两个字符串对象【前提是 String Pool 中还没有 “xxx” 字符串对象】。
- “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
- 而使用 new 的方式会在堆中创建一个字符串对象。
字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以
关键字
final对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final User user = new User();
user.name = "xx";
final声明方法不能被子类重写。【private 方法隐式地被指定为 final:如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。】
final声明类不允许被继承
static:类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
静态方法在类加载的时候就存在了,它不依赖于任何实例。(所以静态方法必须有实现,也就是说它不能是抽象方法。)
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因为这两个关键字与具体对象关联。
静态语句块在类初始化时运行一次。
静态内部类不能访问外部类的非静态的变量和方法。
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
如果不加访问修饰符,表示包级可见。
protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
面向对象
抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。
接口=完全抽象,接口的字段默认都是 static 和 final 的。
一个类可以实现多个接口,但是不能继承多个抽象类。
抽象类与接口:
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用
default
关键字在接口中定义默认方法)。
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值
浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。
深拷贝:拷贝对象和原始对象的引用类型引用不同对象。
集合
Collection接口:List、Queue、Set
Map接口
判断所有集合内部的元素是否为空,使用 isEmpty()
方法,而不是 size()==0
的方式。
size()有时复杂度不是O(1) eg:ConcurrentLinkedQueue
使用集合转数组的方法,必须使用集合的 toArray(T[] array)
,传入的是类型完全一致、长度为 0 的空数组。
eg:list.toArray(new String[0])
new String[0]
就是起一个模板的作用,方便了解返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型
使用工具类 Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear
方法会抛出 UnsupportedOperationException
异常。
Arrays.asList()
是泛型方法,传递的数组必须是对象数组,而不是基本类型(用原生数据类型数组的话,得到的参数不是数组中的元素,而是对象本身,list.get(0)会为数组地址值)
Arrays.asList()
方法返回的并不是 java.util.ArrayList
,而是 java.util.Arrays
的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法
正确的方法:(推荐)Arrays.stream(arr).collect(Collectors.toList())
RandomAccess
接口中什么都没有定义。所以,在我看来 RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
如:在 binarySearch()
方法中,它要判断传入的 list 是否 RandomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
ArrayList
ArrayList
中只能存储对象,对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。
Array
可以直接存储基本类型数据,也可以存储对象。
对于插入:
- 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O(n)。
- 尾部插入:当
ArrayList
的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。 - 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再把新元素放入指定位置。这个过程需要移动平均 n/2 个元素,因此时间复杂度为 O(n)。
对于删除:
- 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是 O(n)。
- 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O(1)。
- 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
以无参数构造方法创建 ArrayList
时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10
初始化:
private static final int DEFAULT_CAPACITY = 10; //默认初始容量大小
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// TODO:此处可能为String[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
//得到最小应该扩容到的量
private void ensureCapacityInternal(int minCapacity) {
//说明无参构造时,用到才会分配默认内存
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//为什么有这个比较呢?因为这个方法也会被addAll使用
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//扩展1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
System.arraycopy()
与Arrays.copyOf()
方法
// native 方法
/**
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
int[] a = {0,1,2,3,4,5,0};
System.arraycopy(a,1,a,2,5);
Arrays.stream(a).forEach(System.out::println); //0,1,1,2,3,4,5
//主要是为了给原有数组扩容
public static int[] copyOf(int[] original, int newLength) {
// 会申请一个新的数组,并返回
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
ensureCapacity
方法
向 ArrayList
添加大量元素之前用 ensureCapacity
方法,可以减少增量重新分配的次数
//交给用户管理,指定分配的空间容量
public void ensureCapacity(int minCapacity) {
// 这步判断为确定是否初始化,没有初始化的话就需要和默认容量比较
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;//当为无参构造时,如果用户分配的还没默认容量大,就不用管
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
LinkedList
LinkedList 插入和删除元素的时间复杂度?
-
头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
-
尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
-
指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
通过比较索引值与链表 size 的一半大小来确定从链表头还是尾开始遍历。如果索引值小于 size 的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点
Node<E> node(int index) {
// 断言下标未越界
// assert isElementIndex(index);
// 如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找
if (index < (size >> 1)) {
Node<E> x = first;
// 遍历,循环向后查找,直至 i == index
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList 为什么不能实现 RandomAccess 接口?
RandomAccess
是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 LinkedList
底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess
接口。
Set
可以利用 Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List
的 contains()
进行遍历去重或者判断包含操作。(时间复杂度高)
HashSet
的底层数据结构是哈希表(基于 HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
Queue
Deque接口扩展了Queue接口,为双端队列
根据失败后处理方式的不同分为两类:
抛出异常(addFirst/addLast/removeFirst/getFirst…)
返回特殊值(offerFirst/offerLast/pollFirst/peekFirst)
PriorityQueue元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
HashMap
HashMap的长度为什么是2的幂次方?
散列值不能直接拿来用。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
但直接取余效率低,用&可以提升效率,但代价就是除数必须为2的幂次方
取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)
源码中采用的计算方法就是:hash & (n - 1)
(n 代表数组长度)
为什么用&呢?
我们让hash对它取余就是想要hash二进制时最右边的n位数
当数组的长度是2的n次方的时候,减1使后几位变为1,&操作正好也可以达到相同的效果
eg:hash(1101),除数(1000),余数(101)
除数-1(0111),取&,就也得到余数了【这一切的前提就是,除数,也就是数组长度,为2的幂次方】
“拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()
方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。
只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize()
方法对数组扩容。
// 当桶(bucket)上的结点数大于等于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于等于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
负载因子loadFactor:loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
threshold = capacity * loadFactor,当 Size>threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
put方法中的putVal 方法添加元素的分析如下:
- 如果定位到的数组位置没有元素 就直接插入。
- 如果定位到的数组位置有元素就和要插入的 key 比较。如果 key 相同就直接覆盖;如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)
resize进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。
LinkedHashMap
TODO
序列化
将数据结构或对象转换成二进制字节流的过程
属于七层模型中的表示层(数据处理,加密解密),对应的是TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
java自带的serialVersionUID,会检查 serialVersionUID
是否和当前类的 serialVersionUID
一致。如果 serialVersionUID
不一致则会抛出 InvalidClassException
异常。【强烈推荐每个序列化类都手动指定其 serialVersionUID
,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID
。】
**serialVersionUID 不是被 static 变量修饰了吗?为什么还会被“序列化”?**反序列之后,static
变量的值就像是默认赋予给了对象一样,看着就像是 static
变量被序列化,实际只是假象罢了。serialVersionUID
只是用来被 JVM 识别,实际并没有被序列化。
不想序列化用transient
(只修饰变量、反序列后恢复为默认值0)
常用的序列化工具:Kryo…
反射
//获取class对象方式
Class t = t.class
Class t = Class.forName("cn.cui.t");
Class t = instance.getClass();
Class t = ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
需求:知道配置文件信息,根据信息直接创建类并调用方法
通过外部环境配置,在不修改源码的情况下,来控制程序【开闭原则:不修改源码,扩容功能】
Properties properties = new Properties();
properties.load(new FileStream("src\\re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.get("methodname").toString();
Class cls = Class.forName(classfullpath);
Object o = cls.newInstance();
Method method = cls.getMethod(methodName);
method.invoke(o)
Field nameField = cls.getField("age");
System.out.println(nameField.get(o));
Constructor constructor = cls.getConstructor();//无参
Constructor constructor2 = cls.getConstructor(String.class);
加载完类后,堆中就会产生一个Class对象【一个类只有一个对象】,通过该对象就能得到完整的结构信息。
Java程序三个阶段:
【编译阶段】原代码经过javac编译后成为class字节码文件,字节码文件经过类加载器ClassLoader处理之前
通过ClassLoader加载字节码文件,使其变为堆中的Class对象
【加载阶段】成为Class类对象中的一部分,处于堆中,此时变量,构造器,成员方法均已成为类的形式Field,Constructor,Method
【运行阶段】堆中的实例对象连接到它属于的那个Class对象上,从而就可以操作自己的那部分属性
创建对象的两个方向:1.由堆中的Class类对象来new。2.对象本身知道自己属于的Class部分,得到Class对象也可以来创建
缺点:反射基本都是解释执行,对执行速度有影响
Class类:1、Class对象不是new出来的,而是系统创建的(ClassLoader) 2、对于某个类的Class对象,内存中只有一份,因为类只加载一次。3、每个类的实例都会记得自己是由哪个Class实例所生成的。4.通过Class对象可以完整得到一个类的完整结构。5.Class对象存放在堆中的
Class<?> cls = Class.forName(classAllPath);
System.out.println(cls);//显示为哪个类的Class对象 com.cui.Car
System.out.println(cls.getClass());//cls运行类型,java.lang.Class
获取class类对象:
1、编译阶段
Class<?> cls1 = Class.forName(classAllPath);
2、加载阶段【用于参数传递】
Class cls2 = Car.class
3、运行阶段【有对象实例时用】
Class cls3 = car.getClass();
4、通过类加载器获取
ClassLoader classLoader = car.getClass().getClassLoader();
Class cls4 = classLoader.loadClass(classAllPath);
基本数据类型:Class<Integer> integerClass = int.class
包装数据类型:Class<Integer> integerClass = Integer.TYPE
静态加载:编译期间加载相关类,没有则报错【依赖性强,我宁愿什么也不做也不想做错】
动态加载:运行期间加载需要的类(延时加载),如果运行时不用该类,即使不存在该类也不报错【降低了依赖性】
反射用的就是动态加载,其他大多数为静态加载
java文件经过javac编译为字节码文件,字节码文件再进行类加载过程
类加载完毕后:在方法区中生成类的字节码的二进制文件(实际数据),在堆中生成类的Class对象(实际数据的访问入口)
类加载过程:JVM负责完成加载和连接过程
加载(类加载器把字节码文件从不同的数据源转化为二进制字节流加载到内存,并生成代表该类的Class对象)
连接(验证-准备-解析)(将类的二进制数据合并到JRE中)【验证:确保不为危害虚拟机;准备:为静态变量分配内存并初始化,这些都放在方法区中;解析:将符号引用替换为直接引用】
初始化(JVM负责对类进行初始化,主要是静态成员)【执行clinit方法,按源文件出现顺序来完成静态变量的赋值操作与静态代码块中的语句】
//在准备阶段
//a不会被分配内存,b分配内存默认初始化为0,c视为常量赋值为30
public int a = 10;
public static int b = 20;
public static final int c = 30;
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
IO流
内存与磁盘的交互用IO流
创建文件:File
类中的createNewFile
获取文件信息:getName/getAbsolutePath/getParent/length...
目录操作:mkdir/mkdirs/delete
字节流(8bit,1byte):二进制文件 InputStream OutputStream
字符流(按编码):文本文件 Reader Writer
中文文本用字节流会产生乱码
int readData = 0;
try{
FileInputStream fileInputStream = new FileInputStream("filePath");
while((readData = fileInputStream.read()) != -1){ //返回值为读到的内容
System.out.println((char)readData);
}
}...
byte[] buf = new byte[8];
int readLen = 0;
try{
FileInputStream fileInputStream = new FileInputStream("filePath");
while((readLen = fileInputStream.read(buf)) != -1){ //返回值即为读到的长度
System.out.println(new String(buf,0,readLen));
}
}...
FileOutputStream(filePath) //覆盖原来内容
FileOutputStream(filePath,true) //追加内容
节点流直接和数据源连接,为底层流
处理流包装节点流,不会与数据源直接连接(修饰器设计模式),更加灵活
缓存处理流:BufferedReader/BufferedWriter...
【IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。】
对象处理流:处理可序列化的对象 ObjectOutputstream
转换处理流:处理编码不同的问题 InputStreamReader
NIO(TODO)
补充:Java中常见的3种IO模型
BIO(Blocking IO):同步阻塞 IO 模型,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
同步非阻塞IO模型:在准备数据到数据就绪期间,应用程序会一直发起 read 调用,多次发起调用期间不阻塞【轮询】。直到内核空间开始拷贝数据,从内核空间拷贝到用户空间的这段时间里,线程是阻塞的,直到在内核把数据拷贝到用户空间(CPU开销大)
NIO(Non-blocking IO):I/O 多路复用模型。线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了通过ready通知用户线程,用户线程再发起 read 调用【轮询,因此NIO属于同步非阻塞IO模型的一种】。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端的数据连接。当客户端数据到了之后,才会为其服务
Buffer:使用 NIO在读写数据时,都是通过缓冲区进行操作。
容量(capacity
):Buffer
可以存储的最大数据量,Buffer
创建时设置且不可改变;
界限(limit
):Buffer
中可以读/写数据的边界。写模式下,limit
代表最多能写入的数据,一般等于 capacity
(可以通过limit(int newLimit)
方法设置);读模式下,limit
等于 Buffer 中实际写入的数据大小。
位置(position
):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position
都会归零,这样就可以从头开始读写了。
flip
:将缓冲区从写模式切换到读模式,它会将 limit
的值设置为当前 position
的值,将 position
的值设置为 0。
clear
: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position
的值设置为 0,将 limit
的值设置为 capacity
的值
Channel:
read
:读取数据并写入到 Buffer 中。write
:将 Buffer 中的数据写入到 Channel 中。
AIO(Asynchronous I/O):异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
网络
InetAddress:hostName+ip地址
InetAddress host1 = InetAddress.getByName("LAPTOP-HORT1xxx");
System.out.println(host1); //LAPTOP-HORT1xxx/192.168.xx.xxx
InetAddress host2 = InetAddress.getByName("www.baidu.com");
System.out.println(host2); //www.baidu.com/220.181.xx.xxx
String hostAddress = host2.getHostAddress();
System.out.println(hostAddress); //220.181.xx.xxx
String hostName = host2.getHostName();
System.out.println(hostName); //www.baidu.com
Socket:套接字,把网络连接当成一种流
TCP
Server
ServerSocket serversocket = new ServerSocket(9999);
//持续阻塞等待连接
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
。。。
inputStream.close();
socket.close();
serverSocket.close();
Client
Socket socket = new Socket(InetAddress.getLocalHost(),9999);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
outputStream.close();
socket.close();
netstat -an
查看主机网络情况
内部地址:在内部网络的地址
外部地址:对外展示的统一地址
UDP
DatagramSocket socket = new DatagramSocket(9999);
byte[] msg = new byte[1024];
DatagramPacket packet = new DatagramPacket(msg,msg.length);
socket.receive(packet);//未接收就会阻塞等待
DatagramSocket socket = new DatagramSocket(9998);
byte[] msg = "hello".getBytes();
DatagramPacket packet = new DatagramPacket(msg,msg.length,InetAddress.getLocalHost(),9998);
socket.send(packet);
Servlet的作用:
接受HTTP的request请求,servlet从中解析出请求中封装的一些参数,再发给服务端
服务端接受参数并作出处理后,再返回一些信息到servlet中,servlet再处理生成HTTP的response响应发出去
JSP就是替代Servlet输出HTML的。【JSP的本质其实就是Servlet。只是JSP当初设计的目的是为了简化Servlet输出HTML代码。】
如今JSP已经被freemarker、Thymeleaf等模板引擎替代
java8
lambda表达式:对简单接口的实现
interface MathOperation{
int operate(int a,int b);
}
MathOperation add = (int a,int b) -> a+b;
MathOperation subtraction = (a,b) -> a-b;
方法引用:
List<Integer> names = Arrays.asList(1,2,3,4,5,6,7,8,9);
names.forEach(System.out::println);
函数式接口:将简单函数本身也作为了一种对象(更多的是配合lambda表达式使用)
public static void func(List<Integer> list, Predicate<Integer> predicate){
for(Integer i : list){
if(predicate.test(i)){
System.out.println(i);
}
}
}
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9);
System.out.println("all:");
func(list,t->true);
System.out.println("even:");
func(list,t->t%2 == 0);
默认方法:接口实现方法,实现类可做修改也可不用管(减少了改接口的同时连同实现类也得一起改)
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
流式编程:stream
List<Integer> numbers = Arrays.asList(1,2,3,4,5);
numbers.stream().limit(3).forEach(System.out::println);
Optional 类:是一个可以为null的容器对象
新日期时间API:LocalDateTime。。。
其他
重载:方法名相同,变量数量、类型。。。不同
重写:Overrride
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。
HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。【不实现的话,会使两个值相同的对象无法被认为是相同的,因为他们两的hashcode不同】
编译类型、实现类型
匿名内部类的实际类名:org.example.test.Test$1
四个元注解
@Retention //指定注解的作用范围
Target //可以在哪些地方使用
Document //是否会出现在javadoc中
Inherited //子类会继承父类注解
面试题
一个十进制的数在内存中是怎么存的?
进制补码形式存储:最高位是符号位。正数原、反、补相同。负数的反码符号位不变,其他位取反,负数的补码是它的反码加1。计算时补码相加。
二进制首位为标志位,正数为0,负数为1
原码:二进制转化即可
反码:正数原、反、补相同;负数为标志位不变,其他取反
补码:负数反码+1
计算时补码相加
(14) 0000 1110
(-21)1001 0101 — 1110 1010 — 1110 1011
补码相加:11111001
再转回原码:11111000—10000111(-7)
为啥有时会出现4.0-3.6=0.40000001这种现象?
浮点数值采用二进制系统表示, 而在二进制系统中无法精确地表示分数 1/10。【能直接打印出0.1是因为打印位数不够,本质上还是一个无限循环小数】
大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的
Java支持的数据类型有哪些?什么是自动拆装箱?
基本数据类型、引用类型(包括类、接口、数组)
基本数据类型和引用数据类型之间的自动转换
Integer x = 2 //Integer,valueOf(2)
int y = x //x.intValue()
int 和 Integer 有什么区别
什么是值传递和引用传递?
实参:传递给函数/方法的参数,必须有确定的值
形参:用于定义函数/方法,不需要有确定的值
值传递:方法形参接收的是实参值的拷贝,会创建副本。
引用传递:方法形参接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
Java只有值传递
数组形参会影响到实参是因为:它本身拷贝的就是地址,因此会改变
==比较的是什么?
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
若对一个类不重写,它的equals()方法是如何比较的?
等同于==,直接比较对象的内存地址。
Object若不重写hashCode()的话,hashCode()如何计算出来的?
默认采用本地方法,使用JVM分配的内部地址来生成一个散列码
为什么重写equals时还要重写hashcode?
因为 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
说一下map的分类和常见的情况
Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
主要有四个实现类:
HashMap(最常用的Map):根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;不支持线程的同步
HashTable(不常用):支持线程同步,内部方法均被syn修饰,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。不允许有 null 键和 null 值,否则会抛出 NullPointerException
。
LinkedHashMap:是HashMap的一个子类,保存了记录的插入顺序。先得到的记录肯定是先插入的,也可以在构造时用带参数,按照应用次数排序
TreeMap:实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序
java8新特性
lambda【优点:简单、利于并行运算;缺点:除了并行运算外效率很慢】
方法引用::
、函数式接口Function
、默认方法default
、流式编程
补充
普通的二叉查找树会产生极端情况:全在一边(即全部小于/大于根节点)
红黑树:
一种自平衡的二叉查找树,每个节点非红即黑;
-
根节点总是黑色的;
-
每个叶子节点都是黑色的空节点(NIL 节点);
-
如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
-
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
插入时以红节点插入【如果插入节点是黑色,就一定会违背规则4,插入红色,就可能不需要变色或者旋转】
若打破了以上规则,需要调整[变色]与[旋转]
参考资料
[Java 基础 | CS-Notes (cyc2018.xyz)](http://www.cyc2018.xyz/Java/Java 基础.html#概览)