JAVA基础
一.编程思维和算法构建
1.抽象基类
AbstractCollection
AbstractList
AbstractQueue
AbstractSequentialList
AbstractMap
AbstractSet
详情
2.SOLID原则
单一职责原则(SRP)——— 应该有且仅有一个原因引起类的变更。简单地说:接口职责应该单一,不要承担过多的职责。
开闭原则(OCP)—— 一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。简单地说:就是当别人要修改软件功能的时候,使得他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。
里氏替换原则(LSP)—— 所有引用基类的地方必须能透明地使用其子类的对象。简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。里氏替换原则 LSP 重点强调:对使用者来说,能够使用父类的地方,一定可以使用其子类,并且预期结果是一致的。
接口隔离原则(ISP)—— 类间的依赖关系应该建立在最小的接口上。简单地说:接口的内容一定要尽可能地小,能有多小就多小。在软件设计中,ISP 提倡不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离。
依赖倒置原则(DIP)—— 高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类。细节应该依赖抽象,即实现类不应该依赖于接口或抽象类。简单地说,就是说我们应该面向接口编程。通过抽象成接口,使各个类的实现彼此独立,实现类之间的松耦合。软件设计的 DIP 提倡使用者依赖一个抽象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖抽象接口,倒置过来。
3.常见设计模式
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
工厂模式
简单工厂模式(也叫静态工厂模式) —— 简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础。它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。会违反OOP原则 —— 开闭原则:一个软件实体应当对扩展开放,对修改关闭。
工厂方法模式 —— 工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法将对象的创建延迟到子类。该工厂有不修改工厂类的前提,也就是说不修改已有类,实现对扩展是开发,对修改关闭。
抽象工厂模式 —— 抽象工厂模式提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
三种工厂模式最终目的都是为了解耦。
单例模式
单例模式是指在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性。序列化和反射可以破坏单例模式
详情中有段话打错了:"假设在这里没有第二次判断的话,线程a就会再次创建一个新的对象,所以,要在这里再加一次判断。"中的线程a应改为线程b
装饰模式
允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式(装饰模式是结构模式的一种),它是作为现有的类的一个包装。装饰模式通过将对象包装在装饰类中,以便动态地修改其行为。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
super也忘了,下面的链接不会super看不懂。挂一下super的视频链接
装饰模式详情
模板方法模式
在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。也就是说定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现。模板方法模式可以使子类在不改变算法结构的前提下,重新定义算法的某些步骤。
模板方法模式详情
适配器模式
适配器模式(Adapter Pattern)充当两个不兼容接口之间的桥梁,属于结构型设计模式。它通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
适配器模式详情
二.Java基础语法
1.java数据类型的分类
位(bit):又名 比特位,表示二进制位,是计算中内部数据储存的最小单位。一个二进制位只能表示0和1两种状态。
字节(byte):是计算机中处理数据的基本单位。一个字节等于八位(1Byte = 8bit)。
字(word):计算机进行数据处理时,一次存取、加工和传送的数据长度。在常见的计算机编码格式下,一个字等于两个字节(十六位)(1word = 2Byte = 16bit)
JAVA中的数据类型分为两大类:
1、基本数据类型:整型、浮点型、字符型、布尔型
整数类型 —— byte、short、int、long,
浮点类型 —— float、double
字符类型 —— char
布尔类型 —— boolean
2、引用数据类型:接口(interface)、数组([ ])、类(class)。
2.java整数类型长度
byte 8位
short 16位
int 32位
long 64位
3.java浮点数类型遵从标准的浮点规则
详情
按照这套规则,0无法被浮点数精确表示出来,所以规定出最小的正整数是0
PS:现在想想double转float小数位不一样大概是尾数丢失了的原因。
4.java基本数据类型转换
自动类型转换,小容量自动赋值给大容量
强制类型转换,大容量赋值给小容量,可能导致的的结果就是精度损失
char型,向高级类型(整型)转换时,会转换为对应ASCII码值
5.布尔类型的转换
布尔类型与整数类型之间的转换 —— Java中的布尔类型可以与整数类型之间进行转换。当将布尔类型转换为整数类型时,true表示1,false表示0。同样地,将整数类型转换为布尔类型时,非零值将被转换为true,而零值将被转换为false。
布尔类型与字符串类型之间的转换 —— Java中的布尔类型还可以与字符串类型之间进行转换。可以使用Boolean类的静态方法toString()将布尔类型转换为字符串类型,并可以使用Boolean类的静态方法parseBoolean()将字符串类型转换为布尔类型。
6.java变量的作用域
在JAVA中,变量的作用域分为四个级别:类级、对象实例级、方法级、块级。
类级变量 —— 又称全局级变量或静态变量,需要使用static关键字修饰。类级变量在类定义后就已经存在,占用内存空间,可以通过类名来访问,不需要实例化。
对象实例级变量 —— 成员变量,实例化后才会分配内存空间,才能访问。成员变量是定义在方法之外,类之内的。成员变量随着对象的创建而存在,随着对象的消失而消失。
方法级变量 —— 在方法内部定义的变量,就是局部变量。局部变量在调用了对应的方法时执行到了创建该变量的语句时存在,局部变量的作用域从它被声明的点开始,一旦出了自己的作用域马上从内存中消失。
块级变量 —— 定义在一个块内部的变量,变量的生存周期就是这个块,出了这个块就消失了,比如 if、for 语句的块。块是指由大括号包围的代码。
7.java中运算符的优先级
8.泛型
泛型的类型参数只能是类类型,不能是简单类型
不能对确切的泛型类型使用instanceof操作,也就是泛型不能放在instanceof的右边,只能放在instanceof的左边
使用定义的泛型类,就一定要传入泛型类型实参么(泛型的形参和实参)?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。(这句话有点抽象,可以看的详情链接的《4. 泛型的使用》)
泛型只在编译阶段有效,泛型信息不会进入到运行时阶段。所以泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。例如List<String>和List<Integer>看似不是一个类型,在java类型判断中却是一个类,因为编译后变成了List和List。java泛型反编译后结果
当实现泛型接口的类,未传入泛型实参时,与被实现的泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。即:class FruitGenerator<T> implements Generator<T>(不知道这里能不能写FruitGenerator<T, E>这种东西,没时间研究。)。如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
当实现泛型接口的类,传入泛型实参时(public class FruitGenerator implements Generator<String>),则所有使用泛型的地方都要替换成传入的实参类型。即:Generator<T>中有个方法叫public T next()需在实现类FruitGenerator中变为public String next()。
Integer是Number的子类,但是Generic
泛型通配符 —— 一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参。此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。如public <T> T genericMethod(Class<T> tClass) ,public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T,也就是说第一个T是用来声明类型参数的,后面的T才是泛型的实现。需要注意的是在public class Genneric<T>中声明了一个方法 public T getKey()这个方法不叫泛型方法,只是普通的成员方法。更多特例参照下方详情中的《4.6.1 泛型方法的基本用法和4.6.2 类中的泛型方法》
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。静态和非静态之分就在于静态是编译时类型,动态是运行时类型。T代表未知类型,如果可以用于静态声明,因为是未知类型,系统没法指定初始值,手动赋值也不行,因为不知道啥类型,只有运行时才可以指定。而泛型存在的意义就是为了动态指定具体类型,增强灵活性和通用性,所以用于静态声明违背了使用原则。
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。—— public void showKeyValue1(Generic<?(T也可以) extends Number> obj)。在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的
泛型数组 —— 在java中是不能创建一个确切的泛型类型的数组的。
如该写法是不能创建的:List<String>[] ls = new ArrayList<String>[10];
该写法是能创建的:List<?>[] ls = new ArrayList<?>[10];或者List<String>[] ls = new ArrayList[10];
详情文章大概一半位置的地方有点问题
Generic
Generic
Generic
9.跳转语句
break、 continue、 throw 和 return。
10.构造方法的特点
当自定义了构造方法后,编译器将不再自动创建不带参数的构造方法 。
11.重写和重载
重写(Overrite) —— 在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除了子类中方法的返回值是父类中方法返回值的子类时,例如父亲返回值是Number类型,子类可以是Number或者Integer等等。)都相同的情况下, 对方法体进行修改,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的(public>protected>default>private,例父亲是public子类就不能是protect)。重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。也就是访问权限不能少,返回值和异常不能多。
重载(Overload) —— 在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。访问权限也不能作为重载依据。
重载(Overload)和重写(Override)的区别 —— 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
三.Java面向对象编程
堆内存是是Java内存中的一种,它的作用是用于存储Java中的对象和数组。
栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量。
对于final static的变量是存放在常量池中的,不涉及到类的加载。
1.Java包
为了更好地组织类,Java提供了包机制,用于区别类名的命名空间。把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。可以使用通配符 * 来引入整个包或包的子包:
2.访问权限修饰符
private,私有的,被private修饰的类、方法、属性、只能被本类的对象所访问。—— 我什么都不跟别人分享。只有自己知道。
default,默认的,在这种模式下,只能在同一个包内访问。 —— 我的东西可以和跟我一块住的那个人分享。
protected,受保护的,被protected修饰的类、方法、属性、只能被本类、本包、不同包的子类所访问。 —— 我的东西我可以和跟我一块住的那个人分享。另外也可以跟不在家的儿子分享消息,打电话
public,公共的,被public修饰的类、方法、属性、可以跨类和跨包访问。 —— 我的东西大家任何人都可以分享。
3.实例化对象的过程
元数据 —— 描述数据的数据,它包含了对数据的定义,结构,格式和属性等信息。在Java中,元数据可以通过注解(Annotation)来实现,注解可以用于描述代码的功能,结构,使用方式等。
1.类加载 —— 包括加载、链接(验证、准备、解析)和初始化三个阶段。2.内存分配。3.初始化零值。4.设置对象头。5.执行构造函数。6.返回对象引用。
静态变量和静态代码块实在1.类加载阶段初始化和运行的。成员变量是在3.初始化零值时赋默认值,在5.执行构造函数阶段使用构造器赋值的。对象引用放在栈中。
4.继承
继承是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力 —— 子类一定具有父类的全部属性与行为,并且与父类相比,拥有的属性更多,具有的行为更加丰富,表示的范围更小。父类又被称为超类。子类又被称为派生类。一个子类只能够继承一个父类。子类无法访问父类的私有属性(无法访问但是拥有),但是可以通过父类的setter,getter方法间接访问。父类的私有方法无法被子类重写。在继承关系之中,如果要实例化子类对象,会默认先调用父类构造,为父类之中的属性初始化,之后再调用子类构造,为子类之中的属性初始化,即:默认情况下,子类会找到父类之中的无参构造方法,相当于在子类的构造器中写上了super()(一定要写在第一行)。若父类没有无参构造器,则子类的构造器中需要声明super()(括号内填父类构造器的参数)。 —— 子类对象在进行实例化前一定会首先实例化父类对象,先有父类对象才有子类对象,先调用父类的构造方法后在调用子类构造方法(没有父亲哪来的儿子)。关于子类和父类的访问修饰符权限在上文的11.重写和重载有提及。
5.封装
在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。修改属性的可见性来限制对属性的访问(一般限制为private)。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
封装的优点 —— 1. 良好的封装能够减少耦合。2. 类内部的结构可以自由修改。3. 可以对成员变量进行更精确的控制。4. 隐藏信息,实现细节。
6.多态
多态体现为父类引用变量可以指向子类对象。即声明的是父类,实际指向的是子类的一个对象。其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类。在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法。例如父类Animal,子类Cat,Dog,此时写Animal a = new Cat()。需要注意的是,父类引用指向子类对象后,通过该引用只能访问到父类的成员和子类重写或覆盖的父类成员,而无法直接访问子类特有的成员(如果有)。因为在编译时,编译器只知道该引用的类型是父类,所以只能看到父类中定义的成员。但是可以通过强制类型转换为子类后,子类的特有的成员就可见了。
编译时多态(静态绑定)—— 指的是方法重载(Method Overloading),即同一个类中可以有多个同名的方法,但它们的参数列表不同。编译器会在编译时根据方法的参数列表决定调用哪个方法。
运行时多态(动态绑定)—— 指的是方法重写(Method Overriding),即子类可以重写父类的方法。在运行时,JVM根据对象的实际类型调用相应的方法。
7.抽象类
在面向对象的概念中,所有的对象都是通过类来描述的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类。抽象类除了不能实例化对象以外,类的其它功能依然存在,成员变量、成员方法和构造函数的访问方式和普通类一样。
如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。Abstract关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。如果一个类包含抽象方法,那么该类必须是抽象类。任何子类必须重写父类的抽象方法,或者声明自身为抽象类。
8.接口
接口可以理解为一种特殊的类,里面全部是由全局常量和公共的抽象方法所组成。接口是解决Java无法使用多继承的一种手段,但是接口在实际中更多的作用是制定标准的。或者我们可以直接把接口理解为100%的抽象类,既接口中的方法必须全部是抽象方法。(JDK1.8之前可以这样理解)。抽象方法只能存在于抽象类或者接口中,但抽象类中却能存在非抽象方法,即有方法体的方法。接口是百分之百的抽象类)。接口被用来实现抽象,而抽象类也被用来实现抽象,为什么一定要用接口呢?接口和抽象类之间又有什么区别呢?原因是抽象类内部可能包含非final的变量,但是在接口中存在的变量一定是final,public,static的。接口中的属性默认为Public Static Final。
9.静态变量和静态方法
类的成员变量可以分为以下两种:静态变量(或称为类变量),指被 static 修饰的成员变量。实例变量,指没有被 static 修饰的成员变量。
static 修饰的成员变量和方法,从属于类。普通变量和方法从属于对象。静态方法不能调用非静态成员,编译会报错。
静态变量 —— 运行时,Java 虚拟机只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配。在类的内部,可以在任何方法内直接访问静态变量。在其他类中,可以通过类名访问该类中的静态变量。注意:静态变量是被多个实例所共享的。
实例变量 —— 每创建一个实例,Java 虚拟机就会为实例变量分配一次内存。在类的内部,可以在非静态方法中直接访问实例变量。在本类的静态方法或其他类中则需要通过类的实例对象进行访问。
成员方法可以分为以下两种 —— 静态方法(或称为类方法),指被 static 修饰的成员方法。实例方法,指没有被 static 修饰的成员方法。
静态方法不需要通过它所属的类的任何实例就可以被调用,因此在静态方法中不能使用 this 关键字,也不能直接访问所属类的实例变量和实例方法,但是可以直接访问所属类的静态变量和静态方法。另外,和 this 关键字一样,super 关键字也与类的特定实例相关,所以在静态方法中也不能使用 super 关键字。
静态代码块指 Java 类中的 static{ } 代码块,主要用于初始化类,为类的静态变量赋初始值,提升程序性能。静态代码块类似于一个方法,但它不可以存在于任何方法体中。静态代码块可以置于类中的任何地方,类中可以有多个静态初始化块。Java 虚拟机在加载类时执行静态代码块,所以很多时候会将一些只需要进行一次的初始化操作都放在 static 代码块中进行。 如果类中包含多个静态代码块,则 Java 虚拟机将按它们在类中出现的顺序依次执行它们,每个静态代码块只会被执行一次。静态代码块与静态方法一样,不能直接访问类的实例变量和实例方法,而需要通过类的实例对象来访问。
在实例方法中可以直接访问所属类的静态变量、静态方法、实例变量和实例方法。
10.静态声明和静态导入
静态声明 —— static关键字。
import static静态导入是JDK 1.5增加的特性,有两种使用方式:1.用.*时,表示导入类中的所有静态属性和方法(import static java.lang.Integer.*;)。2.用静态名称时,表示只导入该静态属性或方法(import static java.lang.Integer.MAX_VALUE;)。
静态导入后就可以直接用名称访问静态属性和方法,而不必用ClassName.methodName()的方式来访问。其目的是为了减少字符输入量,提高代码的可阅读性,以便更好地理解程序。但是,滥用静态导入会使程序更难阅读,更难维护。
11.内部类
12.final修饰变量
final修饰变量,那么此变量即使常量,只能赋值一次,但是final修饰局部变量和成员变量却有所不同。1.final修饰的局部变量必须在使用之前赋值一次才可以被使用。2.final修饰的成员变量在声明时没有赋值的叫做“空白final变量”。空白final变量必须在构造方法或者静态代码块当中初始化。final修饰的变量不能被赋值这种说法是错误的,严格来说,final修饰的变量不可改变,一旦获得了初始值,该final变量的值就不能被重新赋值。
被static修饰过的空白final变量必须在静态代码块当中赋值。没有被static修饰过的空白dinal变量必须在构造方法中进行赋值。当使用final修饰一个基本类型变量时,不能对基本类型变量重新赋值,修饰过后就是不能改变。但是对于引用类型变量来说,它保存的仅仅是一个引用,final只是保证这个引用类型变量所引用的内存地址不会发生改变,如果引用同一个对象,那么这个对象完全可以发生改变。换个理解方式,就是说final保证的是内存地址的指针指向不会改变。对于引用类型变量来说,final保证的只是这个引用类型变量所引用的地址不会改变,也就是说final保证的是这个对象,并不是真正的“内容”。
final修饰的方法不可以被重写,如果在继承当中不希望子类继承父类的某个方法,则可以使用final修饰父类中的方法。如果子类试图重写该方法,将会引发编译错误。如果父类当中有一个private修饰的私有方法,子类中再定义一个和父类中私有方法相同方法名、相同参数列表、相同返回值类型的私有方法,不算是方法重写,而是定义一个新的方法。因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同参数列表、相同返回值类型的方法。被final修饰过的方法不可以在子类当中被重写,但是可以被重载。
final修饰过的类表示该类无法被任何其它的类继承,而对于final修饰过的类来说,该类中的成员可以定义其为final,也可以不是final。对于方法而言,即使没有final修饰,自然也算是final方法,也可以明确的给该方法加上一个final修饰符,这显然没有什么意义。
13.动态绑定
运行时绑定也叫动态绑定,它是一种调用对象方法的机制。Java调用对象方法时,一般采用运行时绑定机制。1.当调用对象方法时,该方法会和该对象的内存地址、运行类型绑定。2.当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。
14.
15.
16.
四.Java异常处理
1.运行时异常RuntimeException和非运行时异常
运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Throwable: 注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
2.Checked Exceptions 和 Unchecked Exceptions
错误(Error)和异常(Exception)是不同的。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM(Java虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需要的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
Exception(异常):是程序本身可以处理的异常。
Exception可以分为checked exceptions和unchecked exceptions。
1.unchecked exceptions(运行时异常),都是RuntimeException类及其子类异常。2.Checked Exceptions(编译异常),是RuntimeException以外的异常,类型上都属于Exception类及其子类。
3.异常栈
异常栈信息的第一行就是抛出这个异常的最原始的位置。
异常栈信息的最后一行就是最开始调用的地方。
如果异常栈信息后面跟着Cause by,就证明抛出当前异常的原因是捕获到了下面的异常。
4.try ... catch ... finally ... return
finally中有return的时候:finally中的语句一定执行,所以只要进了try之后其他的return都不可能执行。
finally中没有return且(try或catch中有return)。finally中的代码会执行,但它不在catch中的return语句的前面,也不在catch中的return语句的后面,而在catch中的return语句的中间执行。如果try和catch的return的是一个基础类型变量时,后面finally中语句即使有对返回的变量进行赋值的操作时,也不会影响返回的值。如果是对象类型,finally里修改了对象 是影响返回结果的。(因为复杂对象传递的是指针 指针指向的内存区域是一样的。)
5.多个catch 捕获异常时执行的顺序
如同if, if else的关系一样只会按顺序进入其中一个。
子类的捕获异常必须放在父类的上面,不然会报错。因为父类的能接异常的范围至少不会比子类少,子类放在父类下面就失去了意义。
6.finally
用于释放资源,在IO流操作和数据库操作中会见到。
一般来说,被finally控制的代码一定会执行;如果在执行到finally之前jvm退出了(比如:System.exit(0),就不再执行了,只要jvm没有退出,finally里面的代码都会执行。
try…finally这种做法的目的是为了释放资源,但是异常没有处理。
7.try-with-resource
try...catch...finally一般是为了关闭流。将需要使用并且使用完关闭的资源的声明和创建放在try的括号里面,执行完该方法会自动关闭这个对象。注意,不论 try 中是否有异常,都会首先自动执行 close 方法,然后才判断是否进入 catch 块。
java9以后只需要在try里声明。
但是catch 块中,看不到 try-with-recourse 声明中的变量。try-with-recourse 中,try 块中抛出的异常,在 e.getMessage() 可以获得,而调用 close() 方法抛出的异常在e.getSuppressed() 获得。
8.OutOfMemoryError
方法区是JVM 所有线程共享。主要用于存储类的信息、常量池、方法数据、静态变量以及即时编译器编译后的代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫非堆。
堆是线程共享的,所有的对象的实例和数组都存放在堆中,任何线程都可以访问。Java的垃圾自动回收机制就是运用这个区域的。
Permanent Generation space(永久保存区域)。永久保存区域主要存放Class(类)和Meta的信息,Class第一次被Load的时候被放入PermGen space区域,Class需要存储的内容主要包括方法和静态属性。
栈存放基本数据类型的数据、引用数据类型的变量名及对象的引用,但是引用的数据与对象并不放在栈中,而是放在堆中。
1.Java Heap 溢出:java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
2.虚拟机栈和本地方法栈溢出:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常这里需要注意当栈的大小越大可分配的线程数就越少。
3.方法区溢出:方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。异常信息:java.lang.OutOfMemoryError:PermGenspace方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
4.运行时常量池溢出:如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
9.StackOverflowError
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。
10.NoSuchMethodError、NoSuchMethodException
NoSuchMethodException 是 Java 反射机制中的异常,表示在尝试通过反射获取方法时,找不到指定的方法。通常发生在调用 Class 对象的 getMethod、getDeclaredMethod 等方法时,当方法名或方法参数不匹配时抛出该异常。
1.方法名错误: 在使用反射获取方法时,如果指定的方法名不存在,就会抛出 NoSuchMethodException。2.参数类型不匹配: 当获取方法时,如果指定了方法名,还需要指定方法的参数类型。如果参数类型与实际方法的参数类型不匹配,也会抛出 NoSuchMethodException。
解决方案:1.检查方法名和参数类型: 确保使用反射获取方法时,指定的方法名和参数类型与实际类中的方法一致。2.使用 getDeclaredMethod: 如果方法是私有的,可以使用 getDeclaredMethod,并通过 setAccessible(true) 设置访问权限,然后调用方法。3.检查类的继承关系: 如果方法是从父类继承的,也要注意在获取方法时查看父类中是否存在该方法。4.检查方法的访问修饰符: 如果要获取私有方法,确保使用了 getDeclaredMethod 并设置了访问权限。
NoSuchMethodError:当在运行时调用一个不存在的方法时,或者类在编译时存在对某个方法的引用,但在运行时该方法不存在时,会抛出NoSuchMethodError。
NoSuchMethodError 主要发生在运行时,表示在调用某个方法时找不到该方法;而 NoSuchMethodException 主要用于反射机制,表示在通过反射获取方法时找不到指定的方法。在编写代码时,尽量避免出现 NoSuchMethodError,因为它通常表示代码中存在一些逻辑错误或者类版本不一致的问题。而 NoSuchMethodException 更多地与反射相关,在动态地使用类的时候可能会遇到。
11.NoSuchFieldError、NoSuchFieldException
NoSuchFieldError:1、项目中有同一个jar包的不同版本,编译和运行时使用了不同的jar包。比如有两个版本1.1.3和1.1.4(有新增属性)。编译的时候使用1.1.4,编译正常通过。但是运行的时候使用的是1.1.3。2、项目中存在和jar包中路径和类名完全相同的类。因为你新增了属性本地类中没有,所以编译的时候会自动使用Jar包中的对象,但是运行的时候则会优先使用本地的对象。
NoSuchFieldException:当我们使用Java的反射API去访问一个类的字段(即属性),如果该字段不存在,则会抛出NoSuchFieldException异常。反射是Java中一种强大的机制,它允许程序在运行时进行自我检查和自我修改。但是,反射也需要谨慎使用,因为它可能会引发各种运行时错误。1.尝试获取一个不存在的字段。2.字段名称错误或者在寻找字段时大小写不正确。3.字段存在于父类而非当前类。4.字段的访问权限限制导致无法访问。
12.NoClassDefFoundError和ClassNotFoundException
NoClassDefFoundError错误的发生,是因为Java虚拟机在编译时能找到合适的类,而在运行时不能找到合适的类导致的错误。例如在运行时我们想调用某个类的方法或者访问这个类的静态成员的时候,发现这个类不可用,此时Java虚拟机就会抛出NoClassDefFoundError错误。与ClassNotFoundException的不同在于,这个错误发生只在运行时需要加载对应的类不成功,而不是编译时发生。简单总结就是,NoClassDefFoundError发生在编译时对应的类可用,而运行时在Java的classpath路径中,对应的类不可用导致的错误。
ClassNotFoundException是在编译的时候在classpath中找不到对应的类而发生的错误。
13.ConcurrentModificationException
并发修改异常指的是在并发环境下,当方法检测到对象的并发修改,但不允许这种修改时,抛出该异常。
14.NumberFormatException
通常在尝试将字符串转换为数字类型时发生,但字符串的格式不符合数字的规范时抛出。1.字符串包含非数字字符。2.字符串为空或为null。3.溢出。
15.SocketException和ConnectException
SocketException:在底层协议中存在错误,如 TCP 错误。
ConnectException:表示无法连接,也就是说当前主机不存在
16.ConnectTimeoutException和ReadTimeoutException
ConnectTimeoutException:是Apache的HttpClient包抛出的超时异常,定义了通过网络与server建立连接的超时时间,Httpclient包中通过一个异步线程去创建与server的socket连接,这就是该socket连接的超时时;
ReadTimeoutException:网络超时
17.assert
使用 assert 关键词紧跟给一个布尔条件进行断言判断,这种方式断言失败时,会抛出 java.lang.AssertionError 异常,但是没有具体的错误信息。断言是一种调试工具,用于在开发和测试阶段检查程序的某些假设是否为真,它是开发者的一个辅助工具,不应该对线上代码的运行产生任何影响。
五.Java数组与集合类型
1.数组和集合的区别
1.数组声明了它容纳的元素的类型,而集合不声明。
2.数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。
3.数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。
4.数组是java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。
2.Collection
Collection 接口有 3 种子类型集合: List、Set 和 Queue。
3.Properties
该类主要用于读取Java的配置文件,不同的编程语言有自己所支持的配置文件,配置文件中很多变量是经常改变的,为了方便用户的配置,能让用户够脱离程序本身去修改相关的变量设置。就像在Java中,其配置文件常为.properties文件,是以键值对的形式进行参数配置的。
4.HashMap和HashTable的区别
1.HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2.Hashtable:key和value都不能为null。HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key值对应的value为null。
3.HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题。Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。
4.ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5.HashMap
HashMap 的初始长度为 16,之后每次扩充变为原来的两倍。HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
HashMap 的长度为什么是 2 的 N 次方呢?取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。
想使用某个自定义类作为 HashMap 的 key,需要注意以下几点:1.如果类重写了 equals 方法,它也应该重写 hashCode 方法。2.如果一个类没有使用 equals,你不应该在 hashCode 中使用它。3.咱们自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。
遍历Hashmap的方式。
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
// 迭代键
for (Integer key : map.keySet()) {
System.out.println("Key = " + key);
}
// 迭代值
for (Integer value : map.values()) {
System.out.println("Value = " + value);
}
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<Integer, Integer> entry = entries.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
通过Java8 Lambda表达式遍历
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
map.forEach((k, v) -> System.out.println("key: " + k + " value:" + v));
HashMap解决hash冲突的方法是拉链法,发生冲突时建立新的结点形成一条链,并在一定情况转化为红黑树。
HashMap的扩展原理是HashMap用一个新的数组替换原来的数组。因为每次都是扩容两倍,所以就算扩展到2^20(1e6级别)的级别也只会被复制2^4 * 2^16 + 2^4 * 2^15 + 2^5 * 2^14 .... + 2^19 = 2^20 + 2^19 * 16 = 2^20 + 2 ^ 23 约等于 9e6的级别, 也可以看做1e7吧(反之不是很大就是了,自己推的不包对。)
6.ConcurrentHashMap
六.Java注解
1.Annotation
从JDK 5.0开始, Java 增加了对元数据(MetaData)的支持, 也就是 Annotation(注解)。Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
1.@Retention只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 可以保留的域, @Retention 包含一个 RetentionPolicy 类型的成员变量, 通过这个变量指定域。1.RetentionPolicy.SOURCE(源码时): 编译器直接丢弃这种策略的注解,注解不会存在.class文件中。2.RetentionPolicy.CLASS(编译时): 编译器将把注解记录在 class 文件中. 当运行 Java 程序时, JVM 不会保留注解,也就是不会再内存中存在 . 说明是类加载器加载了注解,这是默认值。3.RetentionPolicy.RUNTIME(运行时):编译器将把注解记录在 class 文件中. 当运行 Java 程序时, JVM 会保留注解. 程序可以通过反射获取该注解,Runtime是表示注解是在内存中存在的,所以可以用反射去得到该注解 被jvm运用。@Retention(value=RetentionPolicy.RUNTIME)
@Target指定注解用于修饰类的哪个成员。@Target 包含了一个名为 value,类型为ElementType的成员变量。
@Documented用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档。
1.Annotation
从JDK 5.0开始, Java 增加了对元数据(MetaData)的支持, 也就是 Annotation(注解)。Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
七.I/O编程和数据库编程
1.创建文件
new File只会在内存中创建这个文件对象,只有使用了createNewFile方法才会写入硬盘。
String filepath = "我的文件路径";
File file = new File("filepath");
file.createNewFile();//创建文件(需要异常处理)
file.getName();//获取文件名
file.getAbsolutePath();//获取绝对路径
file.getlength();//获取文件字节大小
file.getParent();//文件父级目录。
2.字节流和字符流
字节流可以读取和写入任何数据,因为任何数据最终都能以字节存储;字符流只能操作文本类型的文件,按照字符进行读取和写入,方便对字符的操作。
输入输出流的父类是InputStream和OutputStream。
常用的字节输入输出流: FileInputStream、FileOutputStream、ObjectInputStream、ObjectOutputStream。
重点看一下FileInputStream和FileOutputStream。
FileInputStream有三种构造方式,常见的是前两个。
FileInputStream fis = new FileInputStream("D:\\Demo\\filepath.txt");
FileInputStream fis = new FileInputStream(new File("D:\\Demo\\filepath.txt"));
FileInputStream的read方法有三种入参,read会读取下一个字节内容,当read返回-1时则表明没有字节可读了。字节的取值范围是-128 到 127,所以无法正确表示中文。
int content = 0;
while((content = fis.read()) != -1) {
System.out.println(char(content)); // 将 A, B, C, D, E 依次读取出来
}
// 一次读取多个字节
byte[] bytes = new byte[2];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len)); // 打印 AB, CD, E
}
byte[]数组的作用是"缓存"————我们可以控制每次读取字节的数量,并将读取的字节缓存在数组中。并根据"有效字节的长度"读取出来比如上面的ABCDE,如果一直读下去,这个缓冲数组的内容是:[A, B], [C, D], [E, D], [E, D]...对应的"有效字节的长度"是2, 2, 1, -1...其实,随着指针的移动,每次新读取的字节都会"不完全覆盖"之前的内容————[C, D]->[E, D]时的D就是未被覆盖掉的。
对于一个字节,用char()转换类型即可;如果是byte[]数组,可以用用new String()的构造方法。
追加读写
如图FileOutputStream有五种构造方式,常见的是除第四个以外的构造方法。其中第二个参数是个名为apped的boolean值,默认不填的话是false(重写文件 —— 把文件内容清空再写进去),如果填入true则是续写(不清空,直接在原文件后追加)。
如图FileOutputStream的write方法只支持三种写法。不能直接输出字符串,需要用字符串的.getBytes()方法转化为byte数组后再使用write方法。
字节流更适合底层的输入输出,字符流则更适合处理一个文本文件。
常用的字符输入输出流:FileWriter和FileReader。与FileInputStream不同的是,FileWriter的read方法支持输出字符串类型。
flush:字节流是直接与数据产生交互。字符流在与数据交互之前要经过一个缓冲区。这是为了防止程序频繁调用I/O操作导致性能变差。字节流输出流FileOutputStream和序列化输出流ObjectOutputStream不需要flush()就能直接写入文件。FileWriter字符输出流,BufferedOutputStream缓冲字节输出流,BufferedWriter缓冲字符输出流,OutputStreamWriter转换输出流都需要刷新flush方法才能写到文件上。如果是和Writer相关(内置缓冲区)或者是内置了缓冲数组的流都需要flush()这个过程才能写入文件。需要flush方法是因为数据暂存在了缓冲池内,不刷新会导致文件内缺少部分数据。数据预存到缓冲池中时,只有当数据的长度满足缓冲池中的大小后,才会将缓冲池中的数据成块的发送,若数据的长度不满足缓冲池中的大小,需要继续存入,待数据满足预存大小后再成块的发送。往往在发送文件过程中,文件末尾的数据大小不能满足缓冲池的大小。最终导致这部分的数据停留在缓冲池无法发送。
flush与close的区别:FileWriter和OutputStream的write()方法是不同的———后者是直接写到文件中(因为是字节);后者是写到内存缓冲区,之后再刷新到文件中(因为是字符),而flush和close都有立即把缓冲区的数据刷新到文件中的作用————而close还会释放资源,流对象无法再使用了。话虽如此,一般都是先调用flush在调用close。
输入转换流InputStreamReader是字节流流向字符流的单向桥梁,是Reader的子类。输出转换流OutputStreamWriter是字符流流向字节流的单向桥梁,是Writer的子类。
编码:字符、字符串(能看懂的)--字节(看不懂的)
解码:字节(看不懂的)-->字符、字符串(能看懂的)
编码是依赖于字符集的,一个字符集可以有多个编码实现,就像代码中的接口实现依赖于接口一样。转换流的原理是:字符流 = 字节流 + 编码表。在转换流中选择正确的编码非常的重要,因为指定了编码,它所对应的字符集自然就指定了,否则很容易出现乱码,所以编码才是我们最终要关心的。转换流的是字符流和字节流之间的桥梁,它可对读取到的字节数据经过指定编码转换成字符也可对读取到的字符数据经过指定编码转换成字节。
输入转换流InputStreamReader读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
InputStreamReader(InputStream in):创建一个默认字符集字符输入流。
InputStreamReader(InputStream in, String charsetName):创建一个指定字符集的字符流。如InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("D:\IO\gbk1.txt"),"GBK");
输出转换流OutputStreamWriter同理。
对象流。即序列化和反序列化。