Java基础知识

本系列参考自Java面试小抄以及黑马程序员

数据类型

基本类型:byte、short、int、long、float、double、char、boolean。
引用类型:类、接口、数组、枚举、标注
JVM的内存空间:

  • Heap 堆空间:分配对象
  • Stack 栈空间:临时变量
  • Code 代码区:类的定义,静态资源

static

表明一个成员在没有所属的类的实例变量的情况下被访问。
static变量在类被JVM载入时,对其进行初始化。
static方法不能引用非静态资源,因为非静态资源比其生成晚。
static代码块中的代码在类加载JVM时运行,且只被执行一次。常用来执行类属性的初始化。静态块优先于各个代码块及构造函数。
执行顺序:静态代码块 > 构造代码块 > 构造函数 > 普通代码块

public class init{
	int a;
	public init(){System.out.println("无参构造函数");}
	public init(int a){System.out.println("有参构造函数");}
	{a=10;System.out.println("构造初始块");}
	static{System.out.println("静态初始块");}
	public void method(){System.out.println("普通初始块");}
}

多个类的继承中执行顺序:父类静态块 > 子类静态块 > 父类代码块 > 父类构造器 > 子类代码块 > 子类构造器

面向对象三大特性

封装:将客观事物封装成抽象的类。
继承:使用现有类的所有功能,且无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为子类或派生类。被继承的类称为基类、父类或超类。
多态:父类中定义的属性和方法被子类继承后,可以具有不同的数据类型或表现出不同的行为。

多态

编译时多态,重载overlord,在编译时就已经确定,运行的时候调用的是确定的方法。主要指方法的重载,根据参数列表的不同来区分不同的方法。
运行时多态,重写override,编译时不确定到底调用哪个具体方法。通过动态绑定来实现的,在运行时才把方法调用与方法实现关联起来。

重载overload,指方法名字相同,而参数不同,返回类型可以相同也可以不同。
重写override,方法返回值和形参都不能改变。

多态的必要条件:
继承:必须存在有继承关系的子类和父类。
重写:子类对父类的某些方法进行重新定义。在调用这些方法时会调用子类的方法。
向上转型:需要将子类的引用赋给父类对象。只有这样该引用才能既可以调用父类方法又可调用子类方法。
运行时多态实现方法:子类继承父类extends(对父类方法的重写),类实现接口implements(对接口方法的实现)。

//extends
class father{
	public void del() {
		System.out.println("father!");
	}
}
class sona extends father{
	public void del() {
		System.out.println("sona");
	}
}
public class Solution {
	public static void main(String[] args) {
		father f = new father();
		father a = new sona();
		f.del();//father!
		a.del();//sona
	}
}
******************************
//implements
interface father{
	public void del();
}
class sona implements father{
	public void del(){
		System.out.println("sona");
	}
}

构造器

也叫做构造方法,构造函数。新建一个类,不定义任何构造器,编译器会默认提供一个无参构造器。
构造器支持重载。子类构造器默认调用父类无参构造器。当定义一个有参构造方法,默认的无参构造器消失,此时子类的构造方法需要通过
super("")调用父类构造方法,除非再定义一个无参构造器。

class father{
	public father(int  a) {
		System.out.println("constructor");
	}
}
class sona extends father{
	public sona(int a) {
		super(a);
		// TODO Auto-generated constructor stub
	}
}

抽象类与接口

抽象方法:只有声明,而没有具体的实现。必须使用abstract关键字修饰。
抽象类:如果一个类中含有抽象方法,则称其为抽象类。但是该类和普通类一样,可以拥有成员变量和成员方法。

抽象类与普通类的区别:抽象方法必须为publicprotected。不能用来创建对象。如果一个类继承于抽象类,则子类必须实现父类的抽象方法,否则,需将子类也定义为abstract类。

接口:供别人调用的方法或函数。接口中可以含有变量和方法,但是接口的变量会被隐式指定为public static final变量,而方法会被隐式指定为public abstract方法。接口是一个极度抽象的抽象类。
抽象类和接口的区别:

  1. 抽象类可以提供成员方法的实现细节。接口中只能存在public abstract方法。
  2. 抽象类的成员变量是各种类型的,而接口的成员变量只能是public static final
  3. 抽象类可以有静态代码块和静态方法。接口不行。
  4. 一个类只能继承一个抽象类,但是可以实现多个接口。
  5. 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
  6. 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

创建对象

  1. new
  2. 反射机制
  3. clone机制
  4. 序列化机制

值传递和引用传递

值传递:在方法调用时,将实参复制一份传递到方法中,这样当方法对形参进行修改时不会影响到实参。
引用传递:在调用方法时,将实参的引用传递到方法中,在方法中对形参进行的修改将会影响到实参。
基本类型是值传递。引用类型是也是值传递,只不过值是对应的引用。

int agg=10;//age是基本类型,所以值直接保存在变量中
String name="enheng";//name是引用类型,变量中保存的只是对象的内存地址,这种变量一般被称之为对象的引用

==和equals

==常用于相同的基本数据类型之间的比较。比较的是变量内存的数据是否相等。引用类型存储的是对象的引用,也就是判断两个对线是否指向同一块内存区域。
equals用于两个对象之间,判断一个对象是否等于另一个对象。如果没有重写Object类中的equals()方法,那么其等价于通过==比较两个对象。如果重写此方法,那么判断内容相等,就返回true。重写equals()一般也要求重写hascode()

String, StringBuffer, StringBuilder

String类中使用字符数组保存字符串,用final修饰,所以String对象是不可变的。
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,也使用字符数组保存字符串,但这两种对象是可变的。
线程安全——String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder是非线程安全的。
性能——每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

String为什么要设计成不可变的
便于实现字符串池,在堆中开辟一块存储空间string pool,当初始化一个String变量时,如果该字符串已经存在,则返回已存在字符串的引用。
便于多线程安全。
避免安全问题
加快字符串处理速度。

常量池

全局字符串池 string pool:在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool。实现此功能的是一个StringTable类,它是一个哈希表。
class文件常量池 class constant pool:存放编译器生成的各种常量和符号引用。常量池的每一项常量都是一个表。每个表的第一位都是一个字节的标志位,代表当前该常量属于哪种常量类型。不同常量类型具有不同的结构。
运行时常量池 runtime constant pool:java在执行某个类时,必须经过加载-连接-初始化,连接又包括验证-准备-解析三个阶段。当类加载到内存中后,JVM会将class常量池的内容放到运行时常量池中。class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

String str1 = "abc";  
String str2 = "abc";  
String str3 = new String("def");  
String str4 = "def";  
String str5 = str3.intern();  

System.out.println(str1 == str2);//true  
System.out.println(str3 == str4);//false  
System.out.println(str4 == str5);//true

String str="a" 与 new String("a")的区别
str1:程序会在常量池中查找"abc"字符串,若没有,将其放入常量池,再将地址赋给str1;否则直接将找到的字符串地址赋给str1。
str3:程序会在堆内存中开辟一块新空间存放新对象,同时会将"def"字符串放入常量池。相当于创建了两个对象。
intern():先判断字符串常量是否存在于字符串常量池中,如果存在直接返回该常量;否则,说明该字符串常量在堆中,(JDK1.7)将该对象的引用加入字符串常量池中,(JDK1.6)将该字符串加入到字符串常量区。

包装类型

Java是一个面向对象的语言,基本类型并不具有对象的性质,为了与其他对象“接轨”就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
自动装箱和自动拆箱:将基本数据类型重新转化成对象以及相反的转化。
关于基本类型和包装类型的总结
基本类型的优势:数据存储相对简单,运算效率比较高。包装类的优势:有的容易,比如集合的元素必须是对象类型,满足了java一切皆是对象的思想
声明方式不同,基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
存储方式及位置不同,基本类型是直接将变量值存储在堆栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
初始值不同,基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null
使用方式不同,基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到

基本类型 二进制位数 包装器类
boolean 1 Boolean
byte 8 Byte
char 16 Character
short 16 Short
int 32 Integer
long 64 Long
float 32 Float
double 64 Double

类型转换:

  1. 自动转换:范围小的数据类型可以自动转换为范围大的数据类型
    img
  2. 强制转换
  3. 包装器过渡类型转换
  4. 字符串与其他类型之间的转换
  5. Data和其他数据类型之间的转换

int和Integer区别
Integer是int的包装类,int是基本数据类型
Integer变量必须实例化后才能使用,int变量不需要
Integer实际是对象的引用,指向new的Integer对象,int是直接存储数据值
Integer的默认值是null,int的默认值是0

int a=1000;
Integer b=new Integer(1000);
Integer c=1000;
System.out.println(a == b); // true 因为b会自动拆箱为int,然后就变成了两个int变量的比较。
System.out.println(a == c); // true c也会自动拆箱

System.out.println(b == c); // false 因为c生成的变量指向java常量池中的对象,而b生成的变量指向堆中新建的对象,两者在内存中的地址不同。
//但是如果两者的变量值在[-128,127],则结果比较为true。java会进行自动装箱,然后对值进行缓存,如果下次有相同的值,直接在缓存中取出使用。当值超出此范围,会在堆中new一个新的对象来存储。

反射[[Java基础知识#创建对象|相关]]

反射允许对成员变量,成员方法和构造方法的信息进行编程访问。

  1. 获取class字节码文件对象:3种方法
    1. Class.forName("全类名");//源代码阶段:java文件->class文件。全类名=包名+类名//最为常用的
    2. 类名.class//加载阶段:将class文件加载到内存中//一般更多当做参数进行传递
    3. 对象.getClass();//运行阶段:new对象//当已经有了一个对象后,用它
    4. 基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。
  2. 获取构造方法(Constructor类),成员变量(Field类)和成员方法(Method类):包括修饰符、名字、形参、赋值、甚至运行方法
    setAccessible(true);可暂时关闭JDK的安全检查,临时取消权限的校验。

为什么引入反射概念?
Oracle希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。
使用场合:JDBC的操作中进行数据库的链接。Spring框架的使用。

泛型

将类型参数化,在编译时才确定具体的参数。泛型只存在于编译阶段,而不存在于运行阶段,在编译后的class文件中,是没有泛型这个概念的。

ArrayList<String> a = new ArrayList<String>(); 
ArrayList<Integer> b = new ArrayList<Integer>(); 
Class c1 = a.getClass(); 
Class c2 = b.getClass(); 
System.out.println(c1 == c2);//true

ArrayList<String> names = new ArrayList<>();就使用了泛型。这样当向names里加入数字时,编译器会直接报错。
使用泛型的优点:

  • 类型安全,编译时就可以检查出因类型不正确导致的错误
  • 消除强制类型转换
  • 潜在的性能收益,所有的工作都在编译器中完成,因为泛型的实现方式,不需要JVM或类文件更改。

泛型通配符

  • 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>。无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
  • 固定上边界的通配符(Upper Bounded Wildcards),采用<? extends E>的形式。使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类
  • 固定下边界的通配符(Lower Bounded Wildcards),采用<? super E>的形式。使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.。

注意: 可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

序列化

  • 序列化:将对象写入到IO流中
  • 反序列化:从IO流中恢复对象
  • 意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
  • 使用场景:所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
    序列化实现方式:
  1. Serializable接口:序列化接口没有方法或字段,仅用于标识可序列化的语义。
    序列化步骤
    1. 步骤一:创建一个ObjectOutputStream输出流
    2. 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象
      反序列化步骤
    3. 步骤一:创建一个ObjectIntputStream输出流
    4. 步骤二:调用ObjectInputStream对象的readObject输出可序列化对象

反序列化并不会调用构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
使用transient关键字选择不需要序列化的字段。静态变量也不会被序列化,因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
Java序列化同一对象,并不会将此对象序列化多次得到多个对象:
所有保存到磁盘的对象都有一个序列化编码号当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。如果此对象已经序列化过,则直接输出编号即可。

try (//创建一个ObjectOutputStream输出流            
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {    //将对象序列化到文件s           
		Person person = new Person("9龙", 23);           
		oos.writeObject(person);       
} catch (Exception e) {           
	e.printStackTrace();       
}   

try (//创建一个ObjectInputStream输入流            
  ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) {           
	  Person brady = (Person) ois.readObject();           
	  System.out.println(brady);       
} catch (Exception e) {           
  e.printStackTrace();       
}
  1. Externalizable接口,继承自Serializable。该接口中定义两个抽象方法:writeExternal()readExternal()
实现Serializable接口 实现Externalizable接口
系统自动存储必要的信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
性能略差 性能略好

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
什么情况下需要修改serialVersionUID呢?分三种情况。

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

异常

所有异常都有一个共同祖先java.lang包的Throwable类。里面两个重要子类:ExceptionError
Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception 又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常)。

非受检查异常:包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。例如:NullPointException(空指针)NumberFormatException(字符串转换为数字)IndexOutOfBoundsException(数组越界)ClassCastException(类转换异常)ArrayStoreException(数据存储异常,操作数组时类型不一致)等。
受检查异常:是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检查异常。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException等。
非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。

Error :Error 属于程序无法处理的错误 ,无法通过 catch 来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。

throws 和 throw 区别如下:

  • throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
  • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

常见异常

  • java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
  • java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
  • java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
  • java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
  • java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
  • java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
  • java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
  • java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
  • java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
  • java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
  • java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
  • java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
  • java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
  • java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
  • java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。

try-catch-finally

try只适合处理运行时异常。try+catch适合处理运行时异常+普通异常。编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,加上catch编译器也觉得无可厚。
一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
如果catch中return了,finally依旧会执行。

public static int getInt() {
	int a = 10;
	try {
		System.out.println(a / 0);
		a = 20;
	} catch (ArithmeticException e) {
		a = 30;
		return a;//returen 30。也就是catch保存了现场,然后执行finally,最后调用时恢复现场,返回变量30
	} finally {
		a = 40;
		//return a//如果这样,则直接在finally里return了,catch里的return没有起作用
	}
	return a;
}

IO

img

字符流与字节流的区别

  • 读写的时候字节流是按字节读写,字符流按字符读写。
  • 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
  • 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
  • 只是读写文件,和文件内容无关时,一般选择字节流。

IO的设计模式

  1. 适配器模式
Reader read = new InputStreamReader(inputStream);

img
将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能相互工作。
2. 装饰器模式

new BufferedInputStream(new FileInputStream(inputStream));

动态地往一个类中添加新的行为的设计模式。

posted @ 2023-06-08 13:53  梅落南山  阅读(16)  评论(0编辑  收藏  举报