前言
本文大致复习了Java的基础内容:数据类型,String,运算,类型转换,OOP的封装,继承,多态,Object方法包括equals,hashCode,clone,还有final、static关键字,反射,异常,泛型,注解,枚举类型
参考了github-CYC2018的笔记做的总结,在此基础上补充了一点自己需要补充的内容🖊
一、数据类型
包装类型
八个基本类型:
- boolean / 1
- byte / 8
- char / 16 // 注意!Java的char有16位!因为Java用的是UTF-16BE编码,中文和英文都是2个字节
- short / 16
- int / 32
- float / 32
- long / 64
- double / 64
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱和拆箱完成。
1 | Integer x = 2; // 自动装箱,会调用Integer.valueOf(2)方法,尝试从缓存池获取缓存对象,若已经存在,则取缓存中已存在的Integer值为2的对象(是Integer不是int) |
缓存池
new Integer(123) 与 Integer.valueOf(123) 区别在于:new Integer(123) 每次都会创建一个新对象,而Integer.valueOf(123) 可能会使用缓存对象,因此多次使用Integer.valueOf(123)会取得同一个对象的引用。
1 | Integer x = new Integer(123); |
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
1 | Integer m = 123; |
valueOf() 方法的实现比较简单,先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
二、String
概览
String被声明为final,因此不可继承
内部实现:char数组,数组也是final。意味着value数组初始化之后就不能再引用其他数组。String内部没有改变value数组的方法,保证了String的不可变。
1 | public final class String |
String不可变的好处
- 可以缓存hash值:因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只需要进行一次计算。
- 构成了String Pool常量池:String常量池
- 安全性:String经常作为参数,不可变保证了连接传输的安全
- 线程安全
String的编码
1 | String value = new String(str.getBytes("ISO-8859-1"),"utf-8"); |
String,StringBuffer,StringBuilder
String不可变,所以是线程安全的
StringBuffer非线程安全
StringBuffer线程安全,内部使用synchronized同步
String.intern()
使用 String.intern() 可以保证相同内容的字符串变量引用相同的内存对象。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
1 | String s1 = new String("aaa"); |
如果是采用 “bbb” 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
1 | String s4 = "bbb"; |
在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
三、运算
参数传递
Java参数传递是以值传递,不是引用传递。传进方法的都是值拷贝。传进的值只是拷贝,方法内修改无效,但是修改指针所指的内容是有效的。
隐式类型转换
Java不允许隐式向下转型(包括精度损失下降的基本类型的转换也不允许:double→long->int→short→byte,double→float,float→long都不行)
隐式向下转换的特例:基本类型的+=,/=,-=,*=运算
因为字面量1是int类型,它比short类型精度要高,因此不能隐式将int向下转型为short:
1 | short s1 = 1; |
但是使用 += 运算符可以执行隐式类型转换。
1 | s1 += 1; // +=,/=等都可以。因为其内部做了显式转换,相当于下面显式转换 |
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
1 | s1 = (short) (s1 + 1); // 显式转换OK |
float和double
double不能隐式转换为float !
1.1属于double类型,不能直接将1.1赋值给float变量,因为这是向下转型,Java不能隐式向下转型,因为这会造成精度下降。
1 | // float f = 1.1 // 注意!错误的转型 |
switch
Java7开始,switch语句中可以使用String作为判断条件
1 | String s = "a"; // 声明String |
switch 不支持 long,double,float,是因为 switch 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
Switch支持char, byte, short, int, Character, Byte, Short, Integer, String, or an enum
位运算
优先级从高到低
~
:按位取反
<<
:左移位(低位补0)
>>
:右移位(高位补符号位)
>>>
:无符号右移位(高位补0)
&
:按位与
^
:按位异或
|
:按位或
四、封装、继承、多态
访问权限
java的四个访问权限:private、default、protected、public,(其中default不是访问权限修饰符)
权限 | 类内 | 同包 | 不同包子类 | 不同包非子类 | 备注 |
---|---|---|---|---|---|
private | √ | ×× | ×× | ×× | 类私有 |
default | √ | √ | ×× | ×× | 默认,包访问权限 |
protected | √ | √ | √ | ×× | 保护,包与子类访问权限,修饰成员 |
public | √ | √ | √ | √ | 公开 |
成员可见:其他类可以用这个类的实例对象访问到该成员
类可见:其他类可以用这个类创建实例对象
继承
子类构造函数的约束
子父类中的构造函数的特点:
在子类的构造函数中第一行有默认隐式的super();
所以在子类构造对象时,发现,访问子类构造函数时,父类构造函数也运行了。
子类构造函数默认调用的是父类中的空参数构造函数,如需调用父类中的含参构造,可以在子类构造函数中定义super(args)。
如果父类中没有定义空参构造,那么子类构造必须用super(args)明确要调用父类的哪个构造函数。
若子类构造函数使用this()调用了本类构造函数,那么该构造函数的隐藏super()就没有了,因为super()和this()都只能定义第一行,只能有一个。
但子类中一定有其他的构造函数会访问到父类的构造函数。
注意:super()语句必须要定义在子类构造函数的第一行。因为父类的初始化动作要先完成。
重写与重载(多态)
重写(及重写的返回值类型、访问权限要求)
- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法,
- 返回值类型:子类小于或等于父类的返回值类型;
- 方法访问级别:子类不低于父类;子类中重写方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
重载
- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
super关键字
访问父类的关键字
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
1 | public class SuperExtendExample extends SuperExample { |
封装
设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。
1 | public class AccessWithInnerClassExample { |
抽象类和接口
抽象类
抽象类和抽象方法都用abstract
声明。抽象方法一定位于抽象类中。
抽象类不能被实例化,需要继承抽象类才能实例化其子类。这是它和普通类最大的区别。
示例:
1 | public abstract class AbstractClassExample { |
接口
从Java8开始,接口可以拥有默认的方法实现,此时用default
修饰方法体(因为不支持默认方法的接口维护成本太高了,在Java8以前,一个接口想添加新的方法,需要修改所有的实现类;有的可能需要用适配器转接默认实现,如Spring的WebMvcConfig接口和WebMvcConfigAdaptor)
接口的成员(字段和方法)默认都是public的,并且不允许定义为其他访问权限private或protected。
接口的字段默认都是public static final的常量。
示例:
1 | public interface InterfaceExample { |
抽象类和接口比较
- 一个类可以实现多个接口,只能继承最多一个类或者抽象类
- 接口的字段只能是public static final的,抽象类没这个限制
- 接口的方法只能是public的,抽象类没这个限制
简单说,抽象类只是比普通类少了点方法实现,其他无太大差别。而接口不一样,必须是公开的。
抽象类是IS-A关系,子类对象必须能够替换掉父类对象
接口是Like-A关系,是一种契约,不需要完全实现
选择抽象类还是接口?
很多情况下,接口优先与抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且,Java8开始,接口也能有默认方法实现,使得修改接口的成本也变得很低。
使用接口:
需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
需要使用多继承
使用抽象类:
- 需要在几个相关的类中共享代码
- 需要能控制继承来的成员的访问权限,而不都是public
- 需要继承非静态(non-static)和非常量(non-final)的字段。
内部类
内部类的特点
- 内部类提供了更好的封装。只有外部类能访问内部类
- 内部类可以直接访问外部类的属性和方法,即使是private。因为内部类是成员
- 外部类不能直接访问内部类的属性和方法。要用 内部类实例.xxx
内部类分类
内部类有4种:成员内部类,局部内部类,匿名内部类,静态内部类。
成员内部类:作为外部类的一个成员存在,与外部类的属性、方法并列,不能直接访问
非静态内部类中不能存在static成员。
非静态内部类中可以调用外部类的任何成员,不管是静态的还是非静态的
创建:非静态内部类要依靠外部类实例.内部类创建
1
OuterClass.InnerClass inner = new OuterClass().new InnerClass();
局部内部类:在方法中定义的内部类,与局部变量类似,不能加访问限定符。其范围为定义它的代码块。
匿名内部类:在方法中new的接口或抽象类实现。若实现的方法只有一个,可以用lambda表达式代替。
- 无构造方法(因为没有类名)
- 无静态成员或方法
- 无访问修饰符
如下:
1
2
3
4
5
6
7
8public void click(){
//匿名内部类,实现的是ActionListener接口
new ActionListener(){
public void onAction(){
System.out.println("click action...");
}
}.onAction();
}静态内部类:定义在外部类中,用static定义。
- 静态内部类不可以使用任何外围类的非static成员变量和方法。
- 静态内部类不依附外部类,可以直接创建
静态内部类和非静态内部类对比
1 | public class OuterClass { |
内部类参考链接:
- https://blog.csdn.net/suifeng3051/article/details/51791812
- https://www.cnblogs.com/dolphin0520/p/3811445.html
五、Object通用方法
概览
equals()
equal()与==
对于基本类型,==判断两个值是否相等,基本类型没有equals()方法。
对于引用类型,==判断两个实例是否引用指针是否指向同一个对象,而equals()判断引用的对象是否等价。
对于包装类型,要使用equals()比较数值(如IntegerCache只保存-127~128的值,其他的值都为新对象在堆上产生,不能用==,要使用integer.equals()
)
若未重写equals(),equals的实现就是==
hashCode()
hashCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。
等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。 (相当于hashCode将对象分到桶里,而equals再区分到底等不等)
HashSet中会先识别hashCode是否相等,再识别equals是否相等,以此判断两个对象是否为同一个对象。
hashCode()与equal()
重写equals时一定要重写hashCode()方法,保证两个等价的实例的hashCode也相等。
不同时重写equals和hashCode的后果:
- 可能导致数据的不唯一。(HashSet中会认为是不同的对象,导致存储了两次同样的数据)
- 内存泄漏(开发人员在删除HashSet元素时,自认为已经删除一个引用,但其实还保留了另一个相同内容的副本,导致该对象长时间得不到释放,造成内存泄漏)
实现hashCode
理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x
,编译器会自动进行这个优化。
1 | @Override |
toString()
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
clone()
Cloneable
clone() 是 Object 的 protected 方法,一个类不显式去重写 clone()和实现Cloneable接口,其它类就不能调用该类实例的 clone() 方法。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
1 | public class CloneExample implements Cloneable { // 实现Cloneable接口 |
深拷贝和浅拷贝
- 浅拷贝:拷贝实例和实例的引用。拷贝实例和原始实例的引用类型引用同一个对象;
- 深拷贝:拷贝实例引用指向的对象。拷贝实例和原始实例的引用类型引用不同对象。
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象:
1 | public class CloneConstructorExample { |
1 | CloneConstructorExample e1 = new CloneConstructorExample(); |
六、关键字
final
常量
static
静态
七、反射
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
反射可以不通过访问权限访问,破坏了封装性。
获得Class对象(Class)
forName, getClass
Class.forName("Class全类名")
// 1. 使用Class类的forName静态方法
Class<?> klass = ins.getClass()
// 2. 调用某个对象的getClass()方法
Class<?> klass = ClassA.class
// 3. 直接获取某一个类对象的class
Class<?> klass = Integer.TYPE
// 直接获取某一个对象的class
获取构造器(Constructor)
class.getConstructor
通过Class类的getConstructor方法得到Constructor类的一个实例,Constructor类有一个newInstance方法可以创建一个对象实例。
1 | Constructor constructor = clz.getConstructor(args.class); //参数为原方法参数的类型 |
创建实例(Instance)
class.newInstance, constructor.newInstance
先获得class对象,如Class clz
使用Class对象的newInstance()创建Class对象对应类的实例。
Object ins = clz.newInstance()
指定构造器创建实例:先通过Class对象获取Constructor构造器对象,再通过Constructor对象的newInstance()方法创建实例。
1
2Constructor constructor = clz.getConstructor(args.class); //参数为原方法参数的类型
Object obj = constructor.newInstance(args);
获取方法(method)
clz.getMethods();
获取某个Class对象的方法集合(无视访问权限,不包括继承的方法)
Method[] methods = clz.getDeclaredMethods();
获取某个Class对象的公用方法集合(public),包括继承的公用方法
Method[] methods = clz.getMethods()
获取一个特定的方法。其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象
clz.getMethod(String methodName, Class<?>... paramTypes)
clz.getDeclaredMethod(String methodName, Class<?>... paramTypes)
获取成员变量(Field)
getField(String name)
getDeclaredField(String name)
getFields()
getDeclaredFields()
调用方法(method.invoke)
method.invoke(Object obj, Object...args)
// 参数为要调用方法的实例对象和参数列表
使用示例:
1 | public class test1 { // 在此之外声明了一个Testor的类 |
判断是否为某个类的实例(isInstance)
一般地,我们用instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断是否为某个类的实例,它是一个Native方法:
1 | public native boolean isInstance(Object obj); // Class对象的方法,用法:clz.inInstance(obj) |
反射创建数组
1 | Object array = java.lang.reflect.Array.newInstance(Class elementClass, int length); //实例array |
八、异常
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。
Error:表示 JVM 无法处理的错误。
Exception:表示程序可捕捉可处理的异常。分为两种:
- 受检异常 :编译期可预测的,必须处理的异常,需要用 try…catch… 语句捕获并处理,或用throws子句抛出,否则编译无法通过。
- 非受检异常 :RuntimeException,运行时异常,不要求强制处理。
关键字
五个关键字,分别是:try
、catch
、finally
、throw
、throws
基本语法
try-catch-finally
finally 在 方法返回之前仍然会执行,会覆盖前面的return。
finally子句可有可无,但每个try语句至少要有一个catch或finally子句。
throw
用throw抛出明确的异常,必须是Throwable类或子类对象(有两种方式获取Throwable对象,1. catch 子句获取 2. new 创建)
程序执行玩 throw 后立即停止,throw后面的语句不被执行,并且通过try-catch语句一层层向外抛出异常,最邻近的try块用来检查它是否含有一个与类型匹配的catch语句,如果发现了匹配的异常块,则转向该语句。
使用 throw 的语句块必须使用 try-catch 或 throws 包住
throws
如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。要做到这点,我们可以在方法声明中包含一个throws
子句。
- 一个
throws
子句列举了一个方法可能引发的所有异常类型。
如:void func() throws IOException , NullPointerException
异常链
自定义异常
用户自定义异常类,只需继承Exception
类即可。
在程序中使用自定义异常类,大体可分为以下几个步骤:
- 创建自定义异常类。
- 在方法中通过
throw
关键字抛出异常对象。 - 如果在当前抛出异常的方法中处理异常,可以使用
try-catch
语句捕获并处理;否则在方法的声明处通过throws
关键字指明要抛出给方法调用者的异常,继续进行下一步操作。 - 在出现异常方法的调用者中捕获并处理异常。
本节参考:Java异常处理-详解
九、泛型
泛型类
泛型方法
边界符
extend super
通配符
?
T
PECS原则
类型擦除
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息
十、注解
Annotation类型定义为@interface。
所有的Annotation会自动继承java.lang.Annotation接口,并且不能再去继承别的类或接口。.
参数成员只能用public或默认(default)这两个访问权修饰
- 参数成员只能用8种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
- 要获取类方法和字段的注解信息,必须通过反射获取 Annotation对象,除此之外没有获取注解的方法
- 注解也可以没有定义成员, 可以用于标记
PS:自定义注解需要使用到元注解
元注解
@Documented –注解是否将包含在JavaDoc中
@Retention –什么时候使用该注解(RUNTIME运行, CLASS类加载, SOURCE编译期)
@Target –注解用于什么地方(可用ElementType的参数)
@Inherited – 是否允许子类继承该注解
常见注解
@Override
java.lang.Override是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。
@Deprecated
Deprecated也是一种标记类型注解。当一个类型或者类型成员或父类使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。
@SuppressWarnings
SuppressWarning不是一个标记类型注解。抑制警告。它有一个类型为String[]的成员,这个成员的值为被禁止的警告名。对于javac编译器来讲,被-Xlint选项有效的警告名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名。
@SuppressWarnings(“unchecked”)
自定义注解实例
编写注解,注解处理器
1 | /** |
十一、枚举类型
返回类型 | 方法名称 | 方法说明 |
---|---|---|
int |
compareTo(E o) |
比较此枚举与指定对象的顺序 |
boolean |
equals(Object other) |
当指定对象等于此枚举常量时,返回 true。 |
Class<?> |
getDeclaringClass() |
返回与此枚举常量的枚举类型相对应的 Class 对象 |
String |
name() |
返回此枚举常量的名称,在其枚举声明中对其进行声明 |
int |
ordinal() |
返回枚举常量的序数(它在枚举声明中的位置,从0开始) |
String |
toString() |
返回枚举常量的名称,它包含在声明中 |
static<T extends Enum<T>> T |
static valueOf(Class<T> enumType, String name) |
返回带指定名称的指定枚举类型的枚举常量。 |
1 | public enum Day2 { |
- 参考资料:Java枚举类型enum
其他注意点
Arrays.asList()
Arrays.asList()返回的List是数组的一个快照,底层数据实际上还是原数组,修改数组会修改这个asList()返回的List。而且不能在返回的List上添加删除元素。
Arrays.asList返回的ArrayList不是我们常用的java.util.arraylist,而是其自己工具类的一个静态私有内部类,没有提供add方法。
因此,除非确信array.aslist后长度不会增加,否则谨慎使用:List abc=Arrays.asList(“a”,”b”,”c”)
Socket
ServerSocket建立监听
Socket
静态域、静态块、构造块。。执行顺序
父类静态变量→父类静态代码块→子类静态变量→子类静态代码块→父类非静态变量→父类非静态代码块→父类构造方法→子类非静态变量→子类非静态代码块→子类构造方法
其中,静态都是按顺序执行的
疑问:这里静态域里的new不会去执行静态块,只执行了构造块(按顺序的)
1 | public class B |
输出结果
1 | 构造块 构造块 静态块 构造块 |
1 | public class B |
输出结果
1 | 静态块 构造块 构造块 构造块 |
1 | public class B { |
执行结果:
1 | 构造块 A静态块 A构造块 静态块 构造块 |