写在前面:从今天起,笔者会将在学习中碰到的一些重要的Java知识点记录下来,以便以后复习回顾。一些内容较多且比较复杂的笔者会单独写成一篇博客,对于一些零零碎碎的知识点就直接记录在本篇文章中。本文会一直持续更新。由于时间先后顺序的关系,笔者会将不同方面的知识点直接继续记录在文章末尾,这样过一段时间文章内容可能会比较乱。如果有必要,笔者会每过一段时间整理一下。
1. String
String是不可变的对象,String 类为 final 型的,因此不能被继承。每当我们创建一个字符串对象时,首先就会检查字符串池中是否存在面值相等的字符串,如果有,则不再创建,直接返回字符串池中对该对象的引用,若没有则创建然后放入到字符串池中并且返回新建对象的引用。
在使用字符串的过程中,推荐使用直接赋值(即String s=”aa”),除非有必要才会新建一个 String 对象(即String s = new String(”aa”))。
2. StringBuffer和StringBuilder
StringBuffer和StringBuilder是可变的,在修改字符串时直接在原对象上修改。StringBuffer是线程安全的,因为StringBuffer的方法上都加了synchronized关键字成为同步方法,也因此一般情况下其速度一般比StringBuilder慢。
String、StringBuffer、StringBuilder三者的使用场景:
①String:在字符串不经常变化的场景中可以使用 String 类,如:常量的声明、少量的变量运算等。
②StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用 StringBuffer,例如 XML 解析、 HTTP 参数解析和封装等。
③StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在单线程的环境中,则可以考虑使用 StringBuffer,如 SQL 语句的拼装、JSON 封装等。
并不是所有的 String 字符串操作都会比 StringBuffer 慢,在某些特殊的情况下,String 字符串的拼接会被 JVM 解析成 StringBuilder 对象拼接,在这种情况下 String 的速度比 StringBuffer 的速度快。如:
String name = ”I ” + ”am ” + ”chenssy ” ;
StringBuffer name = new StringBuffer(”I ”).append(” am ”).append(” chenssy ”);
3. 抽象类与接口
抽象类:
①抽象类是不能实例化的。
②抽象方法必须由子类来进行重写。
③只要包含一个抽象方法的抽象类,该方法必须要定义成抽象类,不管是否还包含有其他方法。
④抽象类中可以包含具体的方法,当然也可以不包含抽象方法。
⑤子类中的抽象方法不能与父类的抽象方法同名。
⑥abstract 不能与 final 并列修饰同一个类。
⑦abstract 不能与 private、static、final 或 native 并列修饰同一个方法。
接口:
①1个 Interface 的所有方法访问权限自动被声明为 public,确切的说只能为 public。
②接口中可以定义“成员变量”,或者说是不可变的常量,因为接口中的“成员变量”会自动变为为 public static final。可以通过类命名直接访问:ImplementClass.name。
③接口中不存在实现的方法。
④实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。
⑤不能使用 new 操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用 (refer to) 一个实现该接口的类的对象。可以使用 instanceof 检查一个对象是否实现了某个特定的接口。例如:if(anObject instanceof Comparable){}。
⑥在实现多接口的时候一定要避免方法名的重复。
抽象类和接口的区别:
①抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
②抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
③接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
④一个类只能继承一个抽象类,而一个类却可以实现多个接口。
从语法上看,抽象类可以拥有任意范围的成员数据,同时也可以拥有自己的非抽象方法,而接口只能有静态、不能修改的成员数据(public static final),方法只能定义不能实现。对子类而言,它只能继承一个抽象类(这是 java 为了数据安全而考虑的),但是却可以实现多个接口。
从设计层次上看,抽象类是对类抽象,而接口是对行为的抽象。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。
4. 内部类
内部类可用于实现多继承。
内部类有如下特性:
①内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
②在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
③创建内部类对象的时刻并不依赖于外围类对象的创建。
④内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
⑤内部类提供了更好的封装,除了该外围类,其他类都不能访问。
5. 静态代码块、构造代码块、构造函数执行顺序
静态代码块,静态,其作用级别为类,构造代码块、构造函数,构造,其作用级别为对象。
①静态代码块,它是随着类的加载而被执行,只要类被加载了就会执行,而且只会加载一次,主要用于给类进行初始化。
②构造代码块,每创建一个对象时就会执行一次,且优先于构造函数,主要用于初始化不同对象共性的初始化内容和初始化实例环境。
③构造函数,每创建一个对象时就会执行一次。同时构造函数是给特定对象进行初始化,而构造代码是给所有对象进行初始化,作用区域不同。
三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。
public class Test {
/**
* 静态代码块
*/
static {
System.out.println("执行静态代码块...");
}
/**
* 构造代码块
*/
{
System.out.println("执行构造代码块...");
}
/**
* 无参构造函数
*/
public Test() {
System.out.println("执行无参构造函数...");
}
/**
* 有参构造函数
*
* @param id
*/
public Test(String id) {
System.out.println("执行有参构造函数...");
}
public static void main(String[] args) {
System.out.println("----------------------");
new Test();
System.out.println("----------------------");
new Test("1");
}
}
执行结果为:
执行静态代码块...
----------------------
执行构造代码块...
执行无参构造函数...
----------------------
执行构造代码块...
执行有参构造函数...
6. final
final常量:①编译期常量,永远不可改变。②运行期初始化时,我们希望它不会被改变。
final方法:所有被 final 标注的方法都是不能被继承、更改的。父类的 final 方法是不能被子类所覆盖的。
final类:被final修饰的类都不能被继承。
理解好final“宏变量”:
public class FinalTest {
public static void main(String[] args){
String s1 = "小明";
String s2 = "小" + "明";
System.out.println(s1 == s2); //true
String str1 = "小";
String str2 = "明";
String s3 = str1 + str2;
System.out.println(s1 == s3); //false
//宏替换
final String str3 = "小";
final String str4 = "明";
String s4 = str3 + str4;
System.out.println(s1 == s4); //true
}
}
7. Java数组
public static void main(String[] args) {
int[] datas = new int[]{1,2,3,4,5};
List list = Arrays.asList(datas);
System.out.println(list.size());
}
上面程序输出1。原因如下:
先看asList()源码:
public static <T> List<T> asList(T... a) {
return new ArrayList<T>(a);
}
注意这个参数:T…a,这个参数是一个泛型的变长参数,我们知道基本数据类型是不可能泛型化的,也是就说 8 个基本数据类型是不可作为泛型参数的。所以在这里数组会当做一个对象来处理,它是可以泛型的,所以上面的程序是把一个 int 型的数组作为了 T 的类型,所以在转换之后 List 中就只会存在一个类型为 int 数组的元素了。当然如果将int改为 Integer,则长度就会变成 5 了。
asList 返回的是一个长度不可变的列表。数组是多长,转换成的列表是多长,我们无法通过 add、remove 来增加或者减少其长度。
Arrays.copyOf 拷贝是浅拷贝,包括数组的 clone() 方法、集合的 clone() 方法都是浅拷贝。
要想实现数组等的深拷贝,可以使用序列化来实现:
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
// 写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
// 分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
// 返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
8. 匿名内部类
使用匿名内部类的注意点:
①使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
②匿名内部类中是不能定义构造函数的。
③匿名内部类中不能存在任何的静态成员变量和静态方法。
④匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
⑤匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要为 final。也就是说:当所在的方法的形参需要被内部类里面使用时,该形参必须为 final。原因是拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用 final 来让该引用不可改变。
9. 泛型
泛型的优势:①编译时更强大的类型检测;②提供自动和隐式的类型转换。
<T>和<?>比较:①不同点:<T>用于泛型的定义,例如 class MyGeneric<T> {...}; <?>用于泛型的声明,即泛型的使用,例如 MyGeneric<?> g = new MyGeneric<>()。②相同点:都可以指定上界和下界。
注意点:
①不能在静态成员中引用封闭类型参数,即不能在静态成员中使用泛型。静态方法则可以使用泛型。
②泛型的类型不能是基本类型。
③无法对类型参数使用instanceof。由于泛型参数类型在运行时都被擦除为 Object,泛型类型都被擦除为原始类,因此 obj instanceof T 和 obj instanceof ArrayList<String>, 这种使用方式都会导致编译不通过。
④不能直接使用new实例化类型参数。不能使用 new T() 这样来实例类型参数对象,一方面是因为泛型参数在运行时都被擦除为 Object,另外一方面,T 的具体类型在运行时才能确定,并不确定他的构造器的参数,故无法使用 new 实例化。同样 new T[5] 这种使用也不能通过编译。解决方法是采用工厂类。使用泛型类型Class,可以通过Class.newInstance()来创建泛型类型的对象。
⑤不能同时继承同一个泛型接口的两个变种。
interface Eat<T> {}
class Fruit implements Eat<Fruit> {}
class Apple extends Fruit implements Eat<Apple> {} // compile error
⑥不能重载方法。
由于擦除的原因,以下代码是不能编译通过的:
public void f(List<String> list) {}
public void f(List<Integer> list) {}
⑦无法使用泛型数组。
Java 中不能创建泛型数组,如下代码编译不通过:
ArrayList<String>[] list = new ArrayList<String>[5]
10. Java集合框架
List: ArrayList, LinkedList, Vector, Stack
Set: HashSet, LinkedHashSet, TreeSet
Queue: PriorityQueue, ArrayDeque
Map: HashMap, LinkedHashMap, TreeMap, HashTable, WeakHashMap
并发
List: CopyOnWriteArrayList
Set: CopyOnWriteArraySet, ConCurrentSkipListSet
Queue: ArrayBlockingQueue, LinkedBlockingQueue, DelayQueue, PriorityBlockingQueue, SynchronousQueue, ConcurrentLinkedQueue
Map: ConcurrentHashMap, ConcurrentSkipListMap
Collections: synchronizedList, synchronizedMap, synchronizedSet, synchronizedSortedMap, synchronizedSortedSet, synchronizedNavigableMap, synchronizedNavigableSet
11. IO
①字节流
(1) InputStream、OutputStream
InputStream抽象了应用程序读取数据的方式
OutputStream抽象了应用程序写出数据的方式
(2) EOF = End 读到-1就读到结尾
(3) 输入流基本方法
int b = in.read(); 读取一个字节无符号填充到int低八位。 -1是EOF
in.read(byte[] buf) 读取数据填充到字节数组buf
in.read(byte[] buf, int start, int size) 读取数据到字节数组buf从buf的start位置开始存放size长度的数据
(4) 输出流基本方法
out.write(int b) 写出一个byte到流,b的低八位
out.write(byte[] buf) 将buf字节数组都写入到流
out.write(byte[] buf, int start, int size) 字节数组buf从start位置开始写size长度的字节到流
(5) FileInputStream --> 具体实现了在文件上读取数据
(6) FileOutputStream实现了向文件中写出byte数据的方法
(7) DataOutputStream/DataInputStream
对“流”功能的扩展,可以更加方便地读取int,long,字符等类型数据
DataOutputStream
writeInt()/writeDouble()/writeUTF()
(8) BufferedInputStream&BufferedOutputStream
这两个流类为IO提供了带缓冲区的操作,一般打开文件进行写入或读取操作时,都会加上缓冲,这种流模式提高了IO的性能
从应用程序中把输入放入文件,相当于将一缸水倒入到另一个缸中
FileOutputStream-->write()方法相当于一滴一滴地把水“转移”过去
DataOutputStream-->writeXxx()方法会方便一些,相当于一瓢一瓢地把水“转移”过去
BufferedOutputStream-->方法更方便,相当于一瓢一瓢先放入桶中,再从桶中倒入到另一个缸中,性能提高
②字符流
(1) 编码问题
(2) 认识文本和文本文件
java的文本(char)是16位无符号整数,是字符的unicode编码(双字节编码)
文件是byte byte byte...的数据序列
文本文件是文本(char)序列按照某种编码方案(utf-8,utf-16be,gbk)序列化为byte的存储结果
(3) 字符流(Reader Writer) ---> 操作的是文本文件
字符的处理,一次处理一个字符
字符的底层仍然是基本的字节序列
字符流的基本实现
InputStreamReader完成byte流解析为char流,按照编码解析
OutputStreamWriter提供char流到byte流,按照编码处理
FileReader/FileWriter
字符流的过滤器
BufferedReader ----> readLine 一次读一行
BufferedWriter/PrintWriter ----> 写一行
③对象的序列化,反序列化
(1)对象序列化,就是将Object转换成byte序列,反之叫对象的反序列化
(2)序列化流(ObjectOutputStream),是过滤流---writeObject
反序列化流(ObjectInputStream)---readObject
(3)序列化接口(Serializable)
对象必须实现序列化接口,才能进行序列化,否则将出现异常
这个接口,没有任何方法,只是一个标准
(4)transient关键字
private void writeObject(ObjectOutputStream s)
throws IOException
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
12.序列化与反序列化
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
知识点:
①在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
②通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化。
③虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。
④序列化并不保存静态变量。
⑤要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
⑥Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
⑦服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
ArrayList的序列化:
①为什么elementData是transient的:ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。
②重写writeObject 和 readObject方法的方式把元素保留下来:writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中。
13.访问控制修饰符
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
①default (即缺省,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
②private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
③public : 对所有类可见。使用对象:类、接口、变量、方法
④protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
我们可以通过以下表来说明访问权限:
修饰符 | 当前类 | 同一包内 | 子孙类 | 其他包 | 其他包子孙类 |
private | Y | N | N | N | N |
default | Y | Y | N | N | N |
protected | Y | Y | Y | N | Y/N(见下面说明) |
public | Y | Y | Y | Y | Y |
protected 需要从以下两个点来分析说明:
①子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
②子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。
14.JSP
生命周期:
①编译阶段:解析JSP文件,将JSP文件转为servlet,servlet容器编译servlet源文件,生成servlet类。
②初始化阶段:加载与JSP对应的servlet类,创建其实例,并调用它的初始化方法。
③执行阶段:调用与JSP对应的servlet实例的服务方法。
④销毁阶段:调用与JSP对应的servlet实例的销毁方法,然后销毁servlet实例。
JSP指令:
①<%@ page ... %>:定义网页依赖属性,比如脚本语言、error页面、缓存需求等等。
②<%@ include ... %>:包含其他文件。
③<%@ taglib ... %>:引入标签库的定义。
JSP动作元素:
①jsp:include 在页面被请求的时候引入一个文件。动态include。
②jsp:useBean 寻找或者实例化一个JavaBean。
③jsp:setProperty 设置JavaBean的属性。
④jsp:getProperty 输出某个JavaBean的属性。
⑤jsp:forward 把请求转到一个新的页面。
⑥jsp:plugin 根据浏览器类型为Java插件生成OBJECT或EMBED标记。
⑦jsp:element 定义动态XML元素
⑧jsp:attribute 设置动态定义的XML元素属性。
⑨jsp:body 设置动态定义的XML元素内容。
⑩jsp:text 在JSP页面和文档中使用写入文本的模板。
JSP隐式对象:
①request:HttpServletRequest类的实例
②response:HttpServletResponse类的实例
③out:JspWriter类的实例,用于把结果输出至网页上
④session:HttpSession类的实例
⑤application:ServletContext类的实例,与应用上下文有关
⑥config:ServletConfig类的实例
⑦pageContext:PageContext类的实例,提供对JSP页面所有对象以及命名空间的访问
⑧page:类似于Java类中的this关键字
⑨Exception:Exception类的对象,代表发生错误的JSP页面中对应的异常对象
15.Servlet
Servlet生命周期:
①Servlet 通过调用 init () 方法进行初始化。
②Servlet 调用 service() 方法来处理客户端的请求。
③Servlet 通过调用 destroy() 方法终止(结束)。
④最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
16.JDBC
编程步骤:
①加载驱动程序,利用Java的反射机制:
Class.forName("com.mysql.jdbc.Driver")
②通过DriverManager获取数据库连接,即调用静态工厂方法创建Connection对象:
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
③通过Connection对象创建Statement对象:
conn.createStatement();
//conn.prepareStatement(sql);
④执行SQL语句
⑤操作SQL语句返回的结果集
⑥关闭数据库资源
17.线程池
线程池处理异常:①try-catch捕获异常;②submit执行,Future.get()接受异常;③重写ThreadPoolExecutor.afterExecute()方法,处理传递的异常引用;④实例化时,传入自己的ThreadFactory,设置Thread.UncaughtException。
18.Happen-Before原则
指令不能重排:Happen-Before原则:
①程序顺序原则:一个线程内保证语义的串行性
②volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
③锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
④传递性:A先于B,B先于C,那么A必然先于C
⑤线程的start()方法先于它的每一个动作
⑥线程的所有操作先于线程的终结(Thead.join())
⑦线程的中断(interrupt())先于被中断线程的代码
⑧对象的构造函数执行、结束先于finalize()方法
19.Java整型类型缓存
在 Java 5 中,为 Integer 的操作引入了一个新的特性,用来节省内存和提高性能。整型对象在内部实现中通过使用相同的对象引用实现了缓存和重用,此规则适用于整数区间 -128 到 +127。这种 Integer 缓存策略仅在自动装箱(autoboxing)的时候有用,使用构造器创建的 Integer 对象不能被缓存。
这种缓存行为不仅适用于Integer对象。针对所有整数类型的类都有类似的缓存机制:
有 ByteCache 用于缓存 Byte 对象
有 ShortCache 用于缓存 Short 对象
有 LongCache 用于缓存 Long 对象
有 CharacterCache 用于缓存 Character 对象
Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。
20.final
final关键字可以用于成员变量、本地变量、方法以及类。
final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
你不能够对final变量再次赋值。
本地变量必须在声明时赋值。
在匿名类中所有变量都必须是final变量。
final方法不能被重写。
final类不能被继承。
final关键字不同于finally关键字,后者用于异常处理。
final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
接口中声明的所有变量本身是final的。
final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
final方法在编译阶段绑定,称为静态绑定(static binding)。
没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
按照Java代码惯例,final变量就是常量,而且通常常量名要大写。
21.异常
try-with-resources:大多数情况下,我们使用finally块来关闭资源,有时我们忘记关闭它们并在资源耗尽时获得运行时异常。这些异常很难调试,我们可能需要查看我们使用该类资源的每个地方,以确保我们关闭它。使用Java 7的try-with-resources,我们可以在try语句中创建一个资源并在try-catch块中使用它。当执行来自try-catch块时,运行时环境会自动关闭这些资源。
Java中的final,finally和finalize的区别:
①final和finally是java中的关键字,而finalize是一种方法。
②final关键字可以与类变量一起使用,以便它们不能被重新分配,类可以避免按类扩展,并且使用方法来避免子类覆盖。
③finally关键字与try-catch块一起使用,以提供始终执行的语句即使出现一些异常,通常最终也会用来关闭资源。
④finalize()方法由垃圾收集器在销毁对象之前执行,这是确保关闭所有全局资源的好方法。
(未完待续)