Java-基础
基础
JDK和JRE的区别
-
JDK:java开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序
-
JRE: java运行时环境,提供给想运行java程序的用户使用的。包含了java虚拟机,java基础类库,是使用java语言编写的程序运行所需要的软件环境
基本数据类型
八种基本类型
- 4种整数类型: byte、short、int、long
- 2种小数类型:float、double
- 1种字符类型: char
- 1种布尔型: boolean
- Byte、Short、Integet、Float、Double、Character、Boolean
整数类型有byte、short、int和long,分别占1、2、4、8个字节
类型名 | 取值范围 |
---|---|
byte | -2^7 ~ 2^7 -1 |
short | -2^15 ~ 2^15 -1 |
int | -2^31 ~ 2^31 -1 |
long | -2^63 ~ 2^63 -1 |
包装类型
自动拆箱和装箱
- 装箱: 将基本类型用它们对应的引用类型包装起来
- 拆箱: 将包装类型转换为基本数据类型
- 装箱过程是通过调用包装器的valueOf方法实现的;拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)
包装类型的作用
包装类是为了方便对基本数据类型进行操作,包装类可以解决一些基本类型解决不了的问题
-
函数需要传递进去的参数为Object类型,传入基本数据类型就不可行
-
集合只能存放引用类型的数据,不能存放基本数据类型
- 集合中实际存放的只是对象的引用,每个集合元素都是一个引用变量,实际内容都放在堆内存或者方法区里面,但是基本数据类型是在栈内存上分配空间的,栈上的数据随时就会被收回的
-
基本类型和包装类型之间可以互相转换,自动装箱拆箱
-
包装类的parse方法可以实现基本数据类型 与 String类型之间的相互转换
面试中相关的问题
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2); // true
System.out.println(i3==i4); // false
- 通过valueOf方法创建Integer对象的时候,如果数值在 [-128,127] 之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象
- i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2); // false
System.out.println(i3==i4); // false
- 在某个范围内的整型数值的个数是有限的,而浮点数却不是
- Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的
- Double、Float的valueOf方法的实现是类似的
Boolean i1 = false;
Boolean i2 = false;
Boolean i3 = true;
Boolean i4 = true;
System.out.println(i1==i2); //true
System.out.println(i3==i4); //true
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
public static final Boolean TRUE = new Boolean(true);
/**
* The <code>Boolean</code> object corresponding to the primitive
* value <code>false</code>.
*/
public static final Boolean FALSE = new Boolean(false)
Integer i = new Integer(xxx) 和 Integer i = xxx
- 第一种方式不会触发自动装箱的过程,而第二种方式会触发
- 第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d); // true
System.out.println(e==f); // false
System.out.println(c==(a+b)); // true
System.out.println(c.equals(a+b)); // true
System.out.println(g==(a+b)); // true
System.out.println(g.equals(a+b)); // false
System.out.println(g.equals(a+h)); // true
- 当 == 运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象
- 如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)
- c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较
创建对象的方式
1. 用new关键字创建对象,需要使用构造器
2. 使用反射机制创建对象
- 使用Class类里的**newInstance()**方法,调用的是 无参 构造方法
- 使用java.lang.reflect.Constructor类里的 newInstance 方法,调用的是 有参 构造方法
3. 通过object类的clone方法
- 需要实现Cloneable接口,重写object类的clone方法。无论何时调用一个对象的clone方法,JVM就会创建一个新的对象,将前面对象的内容全部拷贝进去
- 用clone方法创建对象并不会调用任何构造函数
4. 使用反序列化
- 通过ObjectInputStream的 readObject() 方法反序列化类当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象。为了反序列化一个对象,我们需要让我们的类实现Serializable接口
- 在反序列化时,JVM创建对象并不会调用任何构造函数
JDBC步骤
- 注册驱动
- 建立连接
- 创建运行SQL的语句,运行对象Statement负责运行SQL语句。由Connection对象产生,Statement接口类还派生出两个接口类PreparedStatement和CallableStatement,这两个接口类对象提供了更加强大的数据訪问功能。PreparedStatement能够对SQL语句进行预编译,这样防止了 SQL注入 提高了安全性
- 运行语句。运行对象Statement 或 PreparedStatement 提供两个经常使用的方法来运行SQL语句。
- 处理运行结果。ResultSet对象负责保存Statement运行后所产生的查询结果,结果集ResultSet是通过游标来操作的,
- 释放资源
Java的特性
封装、继承、多态
封装:隐藏对象的属性和实现细节,仅对外公开访问方法,控制在程序中属性的读和写的访问级别
封装的目的
增强安全性和简化编程,使用者不必了解具体的实现细节,而只要通过对外公开的访问方法,来使用类的成员
封装的基本要求
- 把所有的属性私有化
- 对每个属性提供 getter 和 setter 方法
- 如果有一个带参的构造函数的话,那一定要写一个不带参的构造函数
继承:在一个现有类的基础之上,增加方法或重写已有方法,从而产生一个新类
- Java 中 Object类 是所有类的父类
继承和权限
- 子类可以重写父类的方法和同名的成员变量
- 子类不能继承父类中访问权限为 private 的成员变量和方法,也不能继承父类的构造方法
类成员访问修饰符与访问能力之间的关系:
Java类的划分
- 普通类:使用 class 定义且不含有抽象方法的类
- 抽象类:使用 abstract class 定义的类,它可以含有或不含有抽象方法
- 接口:使用 interface 定义的类
上述三种类存在以下的继承规律:
- 一个普通类或一个抽象类,要么继承一个普通类,要么继承一个抽象类,即所谓的单继承
- 一个普通类或一个抽象类或一个接口,可以继承任意多个接口
- 一个普通类继承一个抽象类后,必须实现这个抽象类中定义的所有抽象(abstract)方法,否则就只能被定义为抽象类
- 一个普通类继承一个接口后,必须实现这个接口中定义的所有方法,否则就只能被定义为抽象类
- 抽象类继承抽象类或实现接口,可以不完成实现抽象(abstract)方法或父类接口中定义的方法
继承的目的
对原有类的复用
多态: 继承是多态实现的基础,相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同
绑定: 将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来
- 静态绑定: 在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如 static 方法和 final 方法 (这里也包括 private 方法,因为它是隐式 final 的)
- 动态绑定: 在运行时根据对象的类型进行绑定,由方法调用机制实现
多态就是在动态绑定这种机制上实现的
多态的好处
消除了类之间的耦合关系,使程序更容易扩展
实现多态的三个必要条件
- 继承:在多态中必须存在有继承关系的子类和父类
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法
抽象类和接口
相同点
- 都不能被实例化
- 都可以包含抽象方法
- 有默认实现的方法(Java8 可以用 default 在接口中定义默认方法)
不同点
- 一个类可以实现多个接口,但一个类只能继承一个抽象类
- 接口强调特定功能的实现,抽象类 强调所属关系
- 接口成员变量默认为public static final,必须有初始值且不能被修改
- 接口中的方法默认是 public abstract
- 抽象类的成员变量默认 default,可在子类中被重新定义也可被修改
- 抽象类和抽象方法不能被 final修饰,抽象类是被用于继承的,而用final修饰的类,无法被继承
如果基本功能在不断改变,那么就需要使用抽象类,达到解耦目的
成员变量和局部变量
成员变量
- 成员变量定义在 类 中,在整个类中都可以被访问
- 成员变量存在于对象所在的 堆内存 中
- 成员变量 有 默认初始化值
- 成员变量只能被 对象 调用
局部变量
- 局部变量定义在局部范围内,只在所属的区域有效。如:函数内,语句内
- 局部变量存在于 栈内存 中,作用的范围结束,变量空间会自动释放
- 局部变量 没有 默认初始化值
- 静态变量可以被 对象 调用,还可以被 类名调用
Java8新特性
新特性
- Lambda 表达式 :函数作为一个方法参数
- **方法引用 : ** 直接引用已有的 Java类 或 对象实例的方法 或 构造器,与Lambda联合使用使语言的构造更紧凑简洁,减少冗余代码
- **Stream API : ** 函数式编程风格
- **Optional 类 : ** 解决空指针异常
- **Date Time API : ** 加强对日期与时间的处理
- **默认方法 : ** 接口里面有实现的方法
Lambda表达式
什么是Lambda
- Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构
- Lambda 表达式描述了 匿名方法,可以将其作为 参数 传递给 构造方法 或者 普通方法 以便后续执行
实现原理
public class LambdaTest {
@FunctionalInterface
public interface LambdaDemo{
public void runLambda();
}
public static void doSomething(LambdaDemo demo){
demo.runLambda();
}
public static void main(String[] args) {
doSomething(()->System.out.println("hello world!"));
}
}
- Lambda表达式的原理 :编译器在类中生成一个静态函数,运行时调以内部类形式调用该静态函数
- 编译后生成两个class文件
javap -p LambdaTest
输出所有类和成员,可以看出Lambda 表达式在 Java 8 中首先会生成一个私有的静态函数,这个私有的静态函数干的就是 Lambda 表达式里面的内容javap -v LambdaTest
输出行号、本地变量表信息、反编译汇编代码、当前类用到的常量池等信息- main方法执行了一条invokedynamic指令。invokedynamic出现的位置代表一个动态调用点,invokedynamic 指令后面会跟一个指向常量池的调用点限定符,这个限定符会被解析为一个动态调用点。依据这个可以找到对应的 动态调用引导方法 java.lang.invoke.CallSite
@FunctionalInterface
- 通过
@FunctionalInterface
标记的接口可以通过 Lambda 表达式创建实例 @FunctionalInterface
修饰函数式接口的,要求接口中的抽象方法只有一个,有多个抽象方法编译将报错- 加不加@FunctionalInterface对于接口是不是函数式接口没有影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法
语法
Lambda表达式由三部分组成:参数列表、剪头、主体
()为参数列表,-> 为lambda运算符,{}为方法体
有 两种风格,分别是:
- 表达式 - 风格:
(parameters) -> expression
- 块 - 风格:
(parameters) -> {expression;}
- () -> {}、() -> “abc”、() -> {return “abc”;}
常用的函数式接口
随Lambda一同增加的还有一个java.util.function
包,其中定义了一些常见的函数式接口的
Function
:接受一个输入参数,返回一个结果。参数与返回值的类型可以不同,我们之前的map
方法内的lambda就是表示这个函数式接口的Consumer
:接受一个输入参数并且无返回的操作。比如针对数据流的每一个元素进行打印,就可以用基于Consumer
的lambdaSupplier
:无需输入参数,只返回结果。看接口名就知道是发挥了对象工厂的作用Predicate
:接受一个输入参数,返回一个布尔值结果。在对数据流中的元素进行筛选的时候,就可以用基于Predicate
的Lambda
方法引用
- 当在Lambda表达式中直接调用了一个方法时可以使用,其写法为
目标引用::方法名称
- 指向静态方法的方法引用
Function<String, Integer> fun = s -> Integer.parseInt(s);
Function<String, Integer> fun = Integer::parseInt;
- 指向任意类型实例方法的方法引用
Function<String, Integer> fun = s -> s.length();
Function<String, Integer> fun = String::length;
- 指向现存外部对象实例方法的方法引用
String s = "hello";
Supplier<Integer> len = () -> s.length();
Supplier<Integer> len = s::length;
Stream
Stream的理解
- Stream 是一个来自 数据源 的 元素队列 并支持聚合操作,元素是特定类型的对象形成一个队列,Stream并不会存储元素,而是按需计算
- 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等
- 聚合操作 类似SQL语句一样的操作, 比如filter, map, sorted , limit , collect 等
Stream 和 Collection
- 和以前的Collection操作不同, Stream操作还有两个基础的特征
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现
生成流
- **stream() :**为集合创建串行流
- parallelStream() : 为集合创建并行流
- 并行流就是一个把内容拆分成多个数据块,用不同线程分别处理每个数据块的流
Stream API
- **filter : ** 从流中排除某些元素
- **map : ** 将元素转换成其他形式或提取信息
- **sorted : ** 将流中的元素排序
- **limit : ** 截断流,使其元素不超过给定数量
- **collect : ** 将流转换为其他形式
常见关键字
final、finally、finalize()
final
- final修饰的类不能被继承
- final修饰的方法不能被重写
- final修饰的变量不能被改写
- 如果是基本数据类型的变量,其值不能更改
- 如果是引用类型的变量,对象的引用不能被改变,但是对象内部的属性可以被修改。
finally
- finally 是一个关键字,它经常和 try 块一起使用,用于异常处理。使用 try…finally 的代码块种,finally 部分的代码一定会被执行
finalize
- finalize 是 Object 对象中的一个方法,用于对象的回收方法,一般不推荐使用,finalize 是和垃圾回收关联在一起的,在 Java 9 中,将 finalize 标记为了 deprecated, 如果没有特别原因,不要实现 finalize 方法,也不要指望他来进行垃圾回收
static关键字
- 静态代码块: 静态代码块定义在类中方法外,静态代码块在非静态代码块之前执行 (静态代码块—>非静态代码块—>构造方法) 。 该类不管创建多少对象,静态代码块只执行一次
- 修饰成员变量和成员方法:
- 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以通过类名调用。
- 被static 声明的成员变量 属于 静态成员变量
- 静态变量 存放在 Java 内存模型的方法区。调用格式:类名.静态变量名 类名.静态方法名()
- 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法
== 和equals()
==:
- ==: 对于基本数据类型,判断的是两边的值是否相等
- 对于引用类型,判断两个对象是否指向了 同一块内存区域
equals():
- equals():它的作用也是判断两个对象是否相等,但有两种使用情况
- 情况1: 没有override equals(),则等价通过 == 比较这两个对象
- 情况2: override equals()。例如String中的equals(),比较两个对象的内容是否相等
重载和重写的区别
型构 :参数的数量、类型、出现的顺序,不包括方法的返回值类型和访问权限修饰符以及其他修饰符(abstract、static、final)
- 重载(overloading) :同一个类 中定义了 一个以上具有相同名称,但是型构不同的方法
- 重写(overriding): 继承情况下,子类中定义了与其父类中方法具有相同型构的新方法,就称为子类把父类的方法重写了。 这是实现多态必须的步骤
构造器可以被重载和重写吗?
- 可以被重载,实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例
- 不可以被重写,子类无法定义与父类具有相同型构的构造器
hashCode()与equals()
hashCode()介绍
- hashCode() 的作用是获取哈希码,它实际上是返回一个 int 整数。作用是 确定该对象在哈希表中的索引位置
- hashCode() 定义在 JDK 的 Object 类中,即 Java 中的任何类都包含有 hashCode() 函数
以HashSet 如何检查重复 为例子来说明为什么要有 hashCode?
- 把对象加入 HashSet 时,HashSet 会先 **计算 **对象的 hashcode 值来判断对象 加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现
- 但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。大大减少了 equals 的次数,相应就大大提高了执行速度
为什么重写equals时必须重写hashCode()
- 两个对象有相同的 hashcode 值,它们也不一定是相等的 。equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
为什么两个对象有相同的hashcode值,它们不一定相等?
- hashCode()所使用的 杂凑算法 也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(碰撞指的是不同的对象得到相同的hashCode)
- hashcode 只是用来缩小查找成本。HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。
String
底层原理
- String不属于 Java基本数据类型,是一个不可变对象。内部属性都是 final 和 private,不能被继承和扩展,表示String对象一经创建后 ,它的值就不能再被修改,任何对String值进行修改的方法就是重新创建一个字符串
- 内部使用一个 final 的 char[] value来存储字符,concat()、substring()都是new 了一个新的String对象
- 不可变对象不能提供任何可以修改内部状态的方法、setter方法也不行
- 不可变对象不是真的不可变,可以通过反射来对其内部的属性和值进行修改,不过一般不这样做
String为什么要设计成不可变
- 不可变天生线程安全
- String常被用作数据库或接口的参数,可变的话也会有安全问题
- 通过字符串常量池可以节省很多空间
equals()
- 首先会判断要比较的两个字符串它们的 引用 是否相等。如果引用相等的话,直接返回true
- 然后判断比较的对象 是否为String的实例 ,判断两个字符串 长度是否相等,判断两个字符串 每个字符是否相等
为什么要重写equals()
- Object提供的equals()只能比较内存的引用,而new String(“”) 是分配到堆空间而不是字符串常量池
String str = "abc"
- 首先检查字符串常量池中有没有 “abc”
- 如果没有则创建一个,然后使 str 指向字符串常量池中的对象
- 如果有则直接指向
String str = new String("abc")
- 会创建一个或两个对象
- 首先 new 关键字会在堆中创建一个对象
- (重复 String str = “abc”)
反射
- 反射 具有 在 运行期 获取 对象信息 的能力
- 例如 : 很多框架是不知道目标对象的Class类型,就需要 动态获取对象的类型
- 在编译期,类型是确定的,但是很多时候在拿不到确定的对象的属性和值的时候,需要运行时动态调用方法或获取属性
反射的优缺点
反射的优点
- 增加程序的灵活性,提高代码的复用率,外部调用方便
- 能够知道一个类的所有属性和方法
反射的缺陷
性能问题:
- 反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用
- 反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。应该避免在经常被执行的代码或对性能要求很高的程序中使用反射
使用反射会模糊程序内部逻辑:
- 反射代码比相应的直接代码更复杂,因而会带来维护问题
内部暴露:
- 反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用。代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化
反射和private
- private 并不是一种安全机制,而仅是一种作用域的声明
- private 的存在可以让一个类从外面看起来不是那么复杂
反射和泛型
- 泛型擦除是有范围的,定义在类上的泛型信息是不会被擦除的
- Java编译器仍在class文件以Signature属性的方式保留了泛型信息
- Type作为顶级接口,Type 下还有几种类型,比如TypeVariable、 ParameterizedType、WildCardType、 GenericArrayType、以及Class。通过这些接口我们就可以在运行时获取泛型相关的信息
反射的使用
使用反射获取一个对象的步骤
//获取类的Class对象实例
Class clz = Class.forName("com.zhenai.api.Apple");
//根据Class对象实例获取Constructor对象
Constructor appleConstructor = clz.getConstructor();
//使用 Constructor 对象的 newInstance 方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
使用反射调用一个方法
//获取类的Class对象实例
//根据Class对象实例获取方法的Method对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
//使用Method对象调用 invoke方法
setPriceMethod.invoke(appleObj, 14);
反射常用API
获取反射中的Class对象
//在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象
//在 Java API 中,获取 Class 类对象有三种方法
//1. 使用 Class.forName 静态方法(需要类的全路径名)
Class clz = Class.forName("java.lang.String");
//2. 使用 .class方法(只适合在编译前就知道操作的Class)
Class clz = String.class;
//3. 使用类对象的getClass()方法
String str = new String("Hello");
Class clz = str.getClass();
通过反射创建类对象
//通过反射创建类对象主要有两种方式:
//通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法
//1.通过Class对象的newInstance() 方法
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
//2. 通过 Constructor 对象的 newInstance() 方法
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
//二者的不同
//通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法
Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("红富士", 15);
通过反射获取类属性、方法、构造器
//Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性
Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) System.out.println(field.getName());
//Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性
Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) System.out.println(field.getName());
泛型
泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入
- 代码更加简洁(不再需要强制转换)
- 程序更加健壮(在编译期间没有警告,在运行期就不会出现ClassCastException异常)
Java泛型的内部原理
- Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件
- 对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码
常用通配符为: T,E,K,V,?
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
List<?>和List<1object>
- List<?>,即通配符类型,其引用变量,同样可以接受任何对应List<1E>的参数化类型,包括List,但不能添加任何元素,保证了安全性和表述性。但不具有表述性,从中取出的元素时Object类型,要通过手动转换才能得到原本的类型
- List<1Object>,即实际类型参数为Object的参数化类型,其引用变量可以接受List,可以添加元素,但不能接受除了其本身外的任何参数化类型(泛型的子类型化原则)
通配符
上界通配符 < ? extends E>
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类
- 如果传入的类型不是 E 或者 E 的子类,编译不成功
- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
下界通配符 < ? super E>
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类
?和 T的区别
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作
// 可以
T t = operate();
// 不可以
? car = operate();
- T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义
- ?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法
区别1:通过 T 来 确保 泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List<? extends Number> dest, List<? extends Number> src)
区别2:类型参数可以多重限定而通配符不行
使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定
区别3:通配符可以使用超类限定而类型参数不行
Class 和 Class<?>区别
泛型类、接口、方法
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
泛型接口
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
泛型方法
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
IO模型
常用的 I/O 模型
BIO
- 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成
- 在客户端连接数量不高的情况下,没问题。当面对十万甚至百万级连接的时候,需要一种更高效的 I/O 处理模型来应对更高的并发量
NIO
编码
编码方式
编码解决的就是字节和字符之间的转化问题
GB2312
它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。这种编码是一个汉字两个字节
GBK
它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字。这个是中国在1995年搞出来的,主要是用于GB2312编码的补充。这种编码依然是一个汉字两个字节
Unicode
把所有语言统一起来合成一个规则。这种编码是定长的字节数
UTF8
- 既然Unicode是定长的字节数,那么存储一个复杂的汉字可能需要三个字节,但是为了保证是2的幂数集,就会自动扩充为4个字节,别看着一个字节之差,存储的字数多了就会极大的浪费空间
- UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成
这些编码方式会为每一个汉字或者是字母建立一个编码库,在编码的时候字母和编码一一对应
乱码问题
为什么会出现乱码?
因为编码和解码是采用了不同的或者是不兼容的编码方案。一个用UTF-8编码的后的字符。再用GBK去解码,由于两个字符集的编码库不一样。同一个汉字在两个编码库的位置也不一样。于是就出现了乱码。
java解决乱码问题
IO流
编码主要是字节和字符之间的转化,java中编码的转换其实就是IO流中的类来实现的
// 首先是IO流实现,这种通过输入输出流可以直接的指定编码规则。
public void convertionFile() throws IOException {
File file = new File("./愚公要移山.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader inReader = new InputStreamReader(fis, "gbk");
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter outReader = new OutputStreamWriter(fos, "utf-8");
//这种输入gbk,输出utf-8肯定会出现错误
}
String
String类中也提供了一些转码的方法,因为String底层保存的其实就是一个一个字节,而且String还有方法直接转化为字符
`public void convertionString() throws UnsupportedEncodingException {
String s = "愚公要移山,码农飞上天";
// 正常情况下转码的过程
byte[] b = s.getBytes("gbk");// 编码
String sa = new String(b, "gbk");// 解码
System.out.println(sa);
// 错误状态下转码的过程
b = sa.getBytes("utf-8");// 编码使用utf-8
sa = new String(b, "gbk");// 解码使用gbk
System.err.println(sa);
}
Charset
这个Charset是javaNIO中的一个类,整个流程就是读取数据,然后转化为byte,也就是字符,然后重新编码成字符
`public void convertionCharset() throws IOException {
Charset charset = StandardCharsets.UTF_8;
// 从字符集中创建相应的编码和解码器
CharsetEncoder encoder = charset.newEncoder();
CharsetDecoder decoder = charset.newDecoder();
// 构造一个buffer
CharBuffer charBuffer = CharBuffer.allocate(64);
charBuffer.put('A');
charBuffer.flip();
// 将字符序列转换成字节序列
ByteBuffer bb = encoder.encode(charBuffer);
// 将字节序列转换成字符序列
bb.flip();
CharBuffer cb = decoder.decode(bb);
}
`