面向对象
什么是面向对象?
面向对象就是当有需求时,找到这个可以解决你需求的东西(类)进行操作。
例如周末放假,肚子饿了,下楼找一个快餐店(类),坐下来点个饭,点饭要找服务员(对象),这个菜服务员是无法做出来的,是需要厨师才能做出来的,服务员找到可以解决这个问题的人,即厨师(对象)。
类与对象之间的概念无绝对的,是相对的,树是一个类,杉树也是一个类,因为杉树也有细分,同理杉树也可以是一个对象,而这个对象是相对于树这个类而言,具体是类还是对象,是视具体业务场景、理解而言。
什么是面向对象编程?
面向对象编程就是找到所要实现这个功能的类,找到了这个类后创建出一个合适的对象,通过对象来调用其所需的功能(行为 / 方法)。
如果没有这个类,则需要自己创建出一个类,在通过对这个类做一些基础的属性(成员变量)定义,再去创建这个类的对象,赋予属性(成员变量)对应的值,在拿着这个对象去调用所定义好的方法(函数)。
项目就是由一个个类组成的,如同人的世界,由黄种人、白种人、黑种人等等组成的,而这些不同的人种有不同的属性(成员变量),而具有绝对属性(成员变量)值的人就是对象了。
面向对象的作用
更加符合了人类的思想,需要什么就去特定的地方找,而到了这个地方,对象需要什么,则由对象调用者(大脑 / 需求)来决定,如果这个地方没有能够实现需求的,那么久自己创造一个,当以后还需要用到时,只需要再次回到这个地方来使用即可。
面向对象与面向过程的区别
面向对象:把数据和对数据的操作方法放在一起,作为一个相互依存的整体,即对象;对同类对象对象抽象出其共性,形成类;类中的大多数数据,只能用本类的方法进行处理,且类通过一个简单的外部接口与外界发生关系;对象与对象之间通过消息进行通信,而程序流程由用户在使用中决定。
面向过程:程序结构是按照功能划分为若干个基本模块,这些模块形成一个树状结构,各个模块之间的关系尽可能的简单,在功能上相对独立,每一个模块内部均是由顺序、选择、循环三种基本结构组成,其模块实现的具体方法是使用子程序,程序的流程是在写程序时就已经决定了,执行时是自顶而下顺序执行,逐步求精。
类区分
常见的有接口、普通类、内部类、抽象类、静态内部类、枚举类、注解类、泛型类。
接口
修饰关键字:interface Test = abstract interface Test
接口是用于顶层设计的一个特殊抽象类,该接口没有构造器,而且接口的形参修饰符,默认是固定的public static final修饰的,方法默认是abstract修饰的,即抽象方法。
接口之间是可以互相继承,并且接口的继承是多继承的,但接口之间是不能互相实现,即只能用继承来实现关联关系。
接口不可以继承或任何一个类,但类可以实现接口多个接口。
接口是没有构造方法,不能用于实例化对象。
实现接口的类,必须要实现该接口的所有方法,除非将该类置为抽象类。
JDK1.8:
default:默认方法,可以带方法体,并且可以在实现了中重写,可以通过接口的引用调用。
static:静态方法,可以带方法体,不可以在实现类中重写,可以通过接口名调用。
default和static不可以在同一接口方法中使用。
普通类
修饰符:class Test
有五大成员,属性(成员变量)、构造器、方法、内部类、代码块。
只能单继承,但可以实现多个接口,并且可以直接创建对象。
注:接口是提高程序的重用性、可扩展性、降低了耦合度,所以实现了多实现(继承)。
/** * 为什么Java类无法多继承 * @author Levi */ public class A { public void test(int i){ System.out.println(i); } } class B { public void test(int i){ System.out.println(i); } } /* * 多基础的情况,根本无法知道调用的是A的还是B的,重写时,也不知道是重写那个的 */ class C extends A,B{ } |
内部类
如同人是一个,里面还有一个器官类。
可以被4中修饰符修饰,也可以被final、abstract、static修饰
可以有属性(成员变量)、方法,也可以调用外部类的属性(成员变量)、方法。
在运行时class文件的名是使用【外部类$内部类】命名的。
可以继承别的类,也可以实现接口,但是不能被别的类继承。
可以和其他外部类同名,但不可以与本外部类同名。
创建内部类对象:
外部类.内部类 对象名 = new 外部类.内部类();
外部类.内部类 对象名 = 外部类.new 内部类();
调用内部类的方法:
new 外部类.内部类().方法;
局部内部类:是调用方法的局部变量要使用final修饰(JDK1.8之前),在JDK1.8后就自动添加了;局部内部类创建对象只能通过外部的方法返回或者在外部方法中使用开始调用内部类的方法。
外部类.this.属性(成员变量)名 / 方法名,来调用或者区分。
抽象类
修饰关键字:abstract class Test
抽象类常用于中层设计,并不是一个具体类,所以是无法实例化,但抽象类是可以有构造器的。
抽象类是可以被继承和实现接口,继承抽象类的子类,必须要实现抽象类的所有抽象方法,以达到抽象类的具体化,除非将子类也设置为抽象类。
抽象类的方法无法确定具体执行的功能,所以抽象方法时没有方法体的,格式:public abstract void test();
静态内部类
修饰符:static class Test
就是在内部类上加static修饰符,其特点是全局唯一,只加载一次,优先于非静态,任何一次修改都是全局性的影响,在使用上并不依赖于实例对象,生命周期属于类级别,从JVM加载开始到JVM结束。
静态内部类不需要有指向外部类的引用,但非静态内部类需要持有对外部类的引用。
静态内部类不能访问外部类的非静态成员,只能访问外部类的静态成员,非静态内部类能够访问外部类的静态和非静态成员。
一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为就在外部类的里面。
静态内部类不用先创建外部类,可以把静态内部类看做外部类的静态变量,使用就不用外部类实例,而非静态就需要先实例化。
枚举类
修饰符:enum Test
枚举类是一种特殊的Java类,适用于一些方法在运行时,它想要的数据不能使任意的,而且必须是一定范围内的值,可以直接使用枚举来解决。
枚举类中声明的每一个枚举值代表枚举类的一个实例对象,而且枚举类也支持静态导入,并且在JDK1.5后,扩展了switch同时也支持传入一个枚举类型。
其中enum关键字就类似class关键字,只不过class关键字默认是继承Object,而enum定义的枚举类默认继承于java.lang.Enum抽象类。
反编译之后会发现其中每一个枚举值都是枚举类的具体实例对象,只不过是静态常量java.lang.Enum类枚举类的特性。
注解类
修饰符:@interface Test
注解类也是一个特殊的Java类,可以对类、属性(成员变量)、方法、参数、包做标注,本身是没有什么具体功能的,是在运行时,可以根据这个标注做统一的处理。
注解大体可以分为三类,标记注解、一般注解、元注解,其本质就是继承了java.lang.annotation.Annotation这个类,其具体实现类是Java运行时产生的动态代理,即在编译器生成类文件时,标注可以被嵌入字节码中,Java虚拟机可以保留标注的内容,在运行时可以获取到标注的内容,从而根据业务做进一步的处理。
在使用时,通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1,通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法,该方法会从memberValues这个Map中检索出对应的值,而memberValues的来源是Java常量池。
泛型类
修饰符:interface/class Test<泛型>
就是可以通过给容器加限定的形式规定只能存储一种类型的对象,就像给容器贴上标签说明该容器只能存储什么样类型的对象,即只存储整数值类型,那么可以用Test<Integer>。
在JDK5之前,对象保存到集合中,就会失去其特性,取出时,通常要手动进行类型的强制转换,这样就不可避免引发程序的一些安全性问题。
也就是在JDK5之后,泛型允许程序在编写代码时,就限制容器要处理的类型,从而把原来程序运行时问题,转变为编译时问题,以此提高程序的可读性和稳定性,特别是大型的项目这一点更为突出。
泛型是提供给javac编译器使用的,用于限定容器的输入类型,让编译器在源代码级别上,阻挡向容器中插入非法数据。
擦除过程:编译器编译完带有泛型的java程序后,生成的class文件中将不在带有泛型信息,以此使程序允许效率不受影响,这个过程称为擦除。
补偿过程:在运行时,通过获取元素的类型进行转换动作,不用在代码中在强制类型转换。
声明好泛型类型后,只能存放特定的类型元素。
泛型类必须是引用类型。
使用泛型后取出元素不需要类型转换。
注: Java是面向对象的语音,而基本数据类型则不是对象,于是每一个接班类型都有对应的包装类。
类
Java类的五大成员有属性(成员变量)、构造器、方法、内部类(局部内部类)、代码块。
注:这五大成员被不同的关键字修饰会有不同的效果,如同穿上不同颜色的衣服会产生不同的效果。
接下来一一说明。
属性(成员变量)
也称为实例变量,代表这个类的信息,而这个信息的表现可以通过方法(行为),表现出来的形态取决于对象,而且属性(成员变量)的类型也取决于这个类的用途。
构造器
也称为构造器,是用来初始化属性(成员变量)的,因为有一些属性(成员变量)在类一开始就需要进行赋值,如同人一出生就有了确定的血型。
方法(函数)
就是一个类的表现形态(行为),方法经常和属性(成员变量)共同使用,方法也是一个功能的实现,而这个方法的具体产出结果,则是由对象的调用者来决定。
内部类
同上。
代码块
随着类的构造器加载而加载的,用于属性(成员变量)的初始化。
常见键字用途
static
只要被static修饰都是放于内存中的静态域,并且是随着类的加载而加载,随着类的消失而消失,而且无论创建了多少个对象,始终只有一个,即被static修饰的,都是被所有类所共享的。
JavaJVM解读:https://blog.csdn.net/Su_Levi_Wei/article/details/80239654
static可以修饰属性(成员变量)、方法、代码块、内部类,调用只需要直接 类名.X(被static修饰的)。
被static修饰的方法、代码块、内部类,只能调用static修饰的属性(成员变量)、方法,而非static修饰的方法、代码块、内部类可以调用static修饰的。
/** * 加载顺序 * @author Levi */ public class StaticLoad { protected static void main(String[] args) { System.out.println("==== 开始 ===="); /* * 结果: * A 静态代码块 * B 静态代码块 * A 非静态代码块 * A 构造器 * B 非静态代码块 * B 构造器 * * 解读: * 1、B先去寻找父级,寻找到A类,加载A * 2、static代码块是随着类的加载而加载的,加载A,自然也加载代码块 * 3、找到父类,加载完A后,B加载自己,自然也加载静态代码块了 * 4、B加载自己构造器之前,要先去加载父级A的构造器, * 而非静态代码块是随着构造器的加载而加载的,自然A也就先加载代码块 * 5、A加载完代码块也就在加载自己的构造器 * 6、B在加载完父级A后,回到自身加载构造器,但是由于非静态的代码块的特殊性, * 那么自然先加载代码块,加载完后,在加载自身的。 */ new B(); } } class A{ static { System.out.println("A 静态代码块"); }
{ System.out.println("A 非静态代码块"); }
public A() { System.out.println("A 构造器"); } } class B extends A{ static { System.out.println("B 静态代码块"); } { System.out.println("B 非静态代码块"); } public B() { System.out.println("B 构造器"); } } |
final
可以修饰类、属性(成员变量)、方法、内部类,被final修饰的类、属性(成员变量)、方法、内部类都代表的是最终。
final修饰的类,不可以被继承。
final修饰的属性(成员变量),属性(成员变量)的值不能改,并且属性(成员变量)名要大全部大写。
final修饰的方法,不可以被重写。
final修饰的内部类,不能被其他内部类基础。
final常常和static一起使用,static final表示的是全局。
final也可以用于局部变量、成员变量。
abstract
可以修饰类、方法。
abstract修饰的类,会变成抽象类,可以有构造器(是类都有构造器),但是不可以创建对象。
abstract修饰的类被继承了,如果父类有抽象方法,子类要重写,不重写则子类要声明为abstract。
abstract修饰的方法,那么该类一定是抽象类,并且该方法是没有代码体的,即{}。
abstract不可以和static、final一起使用。
abstract修饰符只有public、protected、default,不可以和private共同使用。
权限修饰符
public:公共,无视包环境。
protected:在不同的包,有继承关系的调用。
default:默认,缺省,在本包下调用。
private:私有,在本类下调用。
package
常作为一个小模块的分类,有利于分层分类,使得重名的类名在编译时不会产生覆盖,解决了类重名的问题,关注类底层加载过程。
import
通常作为类库的类调用时倒入,也作为调用自定义类,在不同的包下进行一个调用,使得代码的复用性更强。
main
作为程序的入口,可以有修饰符、返回值(void / 引用类型 / 基本数据类型)、形参列表。
this
可以修饰属性(成员变量)、方法、构造器,代表的是当前对象,作用是区分成员变量和局部变量,区分本类自身和重载的方法、构造器。
当声明类中的属性(成员变量)、方法、构造器,是代表this这个类的属性(成员变量)、方法、构造器,即调用的是本类的,而不是父类的。
super
用于修饰属性(成员变量)、方法、构造器,常用于与继承关系的子父类中的。
主要用于显示声明调用父类的属性(成员变量)、方法、构造器,一般用于区分子父类之间的属性(成员变量)、方法、构造器(非private修饰),用于显示的调用父类构造器,也可以区分父类重载的构造器。
类的三大特性
封装
就跟之前的例子是一样的,去快餐店吃饭,你不需要知道厨师是怎么做出这个饭,你只需要知道你可以吃到饭,实现你的需求。
封装就是如上的例子,厨师隐藏对客户需求的实现细节,只提供一个窗口给客户,让客户提出需求(set方法),并提取结果(get方法)。
一般是通过Java提供的四种权限修饰符来实现封装,在定义类的属性(成员变量)时,一般会使用private来修饰,从而向外部隐藏,只提供get、set方法给调用者来使用属性(成员变量)。
这样做的好处在于提高重用性,即set一次即可;提高安全性,使得调用者无法直接修改类的属性(成员变量);向外部隐藏细节,调用者不需要底层是怎么运行的,只需要知道结果,另外通过get和set方法,可以过滤非法的数据,如青蛙的手脚都是成双成对的出现的。
对于编程者来说,代码有利于阅读,有利于编写程序,使得程序在同一个属性(成员变量)下可以实现多个功能,便于调用者调用。
可用于属性(成员变量)、方法、构造器,不可以用于代码块,因为代码块是随着类的加载而加载的,以及分为静态和非静态的初始化过程。
构造器和方法都可以构成重载:方法同名,或构造器同名,并且形参类型、个数、顺序不同,但是与返回类型无关。
在set方法或构造器,常用this关键字来区分成员变量和局部变量,以及显示的调用本类的重载方法。
/** * 重写为什么和返回类型无关 * @author Levi */ public class J03OverloadingRetValue {
public String test(String name) { return ""; }
public String test(String name,int age) { return ""; }
/* * 如果和返回类型相关的,那么根本就不知道调用的是哪个方法。 */ public int test(String name,int age) { return 1; } } |
继承
这个继承就是生活中的继承的意识,还是拿上面的快餐店的例子,在快餐店老板退休了,他的儿子继承了这家店,他儿子觉得这装修颜色有点单调,把墙体的颜色改为黄色和红色,他儿子还想把名字给换掉。
子类可以继承父类的属性(成员变量)、方法,但是都受权限修饰符而定(private私有,是无法继承的),当父类无法满足子类时,子类可以重写父类的方法,如改变快餐店的接待方式,另外子类也可以扩展自己的属性(成员变量)、方法。
重写说明:
必须要是继承关系。
父类方法权限修饰符要是符合子类环境,如果不符合则无法重写,即私有方法无法重写,但子类依然可以继承,但限于权限修饰符子类无法使用而已。
子类重写父类方法的方法名、形参列表,必须和父类一样,返回类型必须是父类返回类型相同或是此返回类型的子类。
子类重写的方法的修饰符必须和父类相同或高于父类。
继承的好处就是提高了代码的复用性;符合人类思想,使得类与类之间产生关系,便于程序的编写;提高了可扩展性,子类对父类的内容扩展,使得子类的功能更加富有弹性。
缺点就是耦合性密切。
多态
街道办的工作人员和快餐店老板是很熟悉了,快餐店老板有多个儿子,在去街道办帮其中一个儿子办点事时,快餐店老板是可以代表其儿子,拿的身份证是其儿子的身份证,同理,快餐店老板的儿子在某些情况下也可以代表快餐店老板本人。
多态就是一种事物的多种表现形态,根据继承关系,使得代码的表现形式更多有弹性,这个弹性是指向下和向上转型。
多态使得程序更有弹性,当一个父类被多个子类继承时,接收的类型只需要指明是父类类型就可以了,不需要使用单一的类型来接收,这个就是向上转型,这样就使得父类可以使用子类的方法,使得程序代码更富有健壮性和弹性,子类也不用创造大量的构造方法,大大的提高了代码的效率。
另外当子类继承了父类时,在某些方法的返回值,可以指定是其父类,那么在使用时,可以根据业务逻辑判断要返回的是哪个子类。
多态的条件是,必须要是继承关系,而且子类要重写父类的方法,不然会导致没有意义,最后调用的还是父类的方法。
基础集合
名称 | 特点 | |||||
Iterable | 迭代器接口,实现此接口的对象允许实用foreach语句 | |||||
| Collection | 集合父接口 | ||||
| List | 可重复的有序集合容器,拥有特有的迭代器(ListIterator) | ||||
| ArrayList | 线程不安全,效率高,特点是查找快,增删慢,允许存储null。 底层实用典型的数组来存储,所以每次增删都要移动数组所有元素的下角标,所以增删慢。 | ||||
| LinkedList | 增加了Vector和Stack的特性和方法。 底层采用链表数组的方式,每个元素的第一个交表是指向上一个元素,最后一个角标指向最后一个元素,第一个元素的角标和最后一个元素的角标都是null。 适合频繁增删,不适合查找。 | ||||
| Vector | 古老的集合类,是线程安全的,效率低,现在基本不采用 | ||||
| Stack | 拥有栈特性的线程安全类,特点是FILO,先进后出,后出先进的特性。 | ||||
| Set | 不可重复的无序集合容器 | ||||
| HashSet | 线程不安全,存储元素是无序,不可重复,允许存储null,存取快。 内部为HashCode表数据结构,底层是哈希表 = 数组 + 链表/红黑二叉树(链表元素大于8个之上就转换为二叉树) | ||||
| LinkedHashSet | 不可重复,顺序输出,适合频繁遍历,较少删除、操作。 包含了HashSet集合,但是在此基础上增加了一条链表来记录顺序,所以有序,是一个双链表实现 | ||||
| TreeSet | 不可重复,可以给元素排序,没有索引,不能使用普通的for循环,查找效率高,结构为二叉排序树。 按照添加元素进去的升序(小到大),如果要降序(大到小)进行排序则需要重写Comparator的compare()方法。 如果是自定义类,要求自定义类实现Comparable接口,否则无法添加进集合中,会报错。 如果实现了Comparable接口之后,则在重写comparaTo()方法时,则可以按照需求进行已排序,而comparaTo()接口相等则返回正整数,不相等返回负数。 如果在自定义实现了Comparable接口之后,不符合要求可以使用Comparator接口,重写compare()方法。 Comparable中的compareTo方法比Comparator中的compare方法优先级低。 另外: 如果重写了Comparable或Comparator接口中的方法, 建议同时重写hashCode和equals方法,避免使用Comparable或Comparator接口中的方法,比较时如果相等,而内容不相等不会不让其添加。 注: 建议使用set时,是先计算hashCode码,在进行一个比较,如果使用equals,则先进行equals在计算hashCode码,建议hashCode和equals方法同时重写。 因为hashCode是计算一个元素位于内存存储的一个散列值,但这个值不是惟一的。所以,建议同时重写equals方法,避免出现相同的hashCode码。 两个元素equals相同,那么他们的hashCode码必须相同。 | ||||
Map | 在集合的意义上来说,是一个和Collection同级的接口类。 拥有Key和Value,单位是entry。 Key和Value是成对出现的。 Key是不可重复的(Set)。 Value是可重复的(Collection)。 Key和Value可以同时为null,但key为null只能有一个。 | |||||
| HashMap | 存储无序,可以添加null键和null值 | ||||
| LinkedHashMap | 按照添加进去的key袁术进行排序和遍历 | ||||
| TreeMap | 底层使用二叉树,按照Key所在类的指定属性(成员变量)进行排序,要求Key是同一个类对象,对Key考虑使用定制排序和自然排序。 | ||||
| HashTable | 古老的线程安全实现类,效率低,不能添加null键和null值。 | ||||
| Properties | 常用来处理属性(成员变量)文件。 键和值都String类型。 | ||||
补充
Collections:是一个集合工具类,里面都是静态方法,都是用于操作集合。
java.util.EnumSet:枚举集合类,保证了集合中的元素不重复。
java.utilsEnumMap:key是enum类型,value可以是任意类型。
常用IO流
字节流
类别 | 名称 | 特点 | |||
输入流 |
| ||||
| InputStream |
| |||
对应输出流 | OutputStream | ||||
| FileInputStream | 用于操作电脑中的各种文件都可以,因为电脑的文件都是使用底层二进制码来存储的。 | |||
对应输出流 | FileOutputStream | ||||
| BufferedInputStrem | 缓冲字节流,内部具备一个8K的数组,但是不具备有操作五年级的功能,类似一个大的管道,使得数据传输速度更快。 | |||
对应输出流 | BufferedOutputStream | ||||
| SequenInputStream | 序列输入流(合并流),可以将N个文件的内容合并成一个文件,也可以把一个文件拆分成为多个文件。 | |||
对应输出流 | SequenOutputStream | ||||
| ObjectInputStream | 对象输出流,是根据Java的序列化和反序列化的机制,但是如果是自定义类一定要实现Seriablizable才可以加个这个自定义类的对象永远保存在硬盘中。 建议:在序列化过程中,给予其一个UID值,是的在保存到文件中的状态取出时不会改变,因为当类发生改变时,在取出时,就不会出现报错,因为类发生改变时,再次取出,JVM认为当前类状态不一致。 而JVM计算出来的UID是根据成员变量来计算的,与方法无关,所以建议自定义一个,而且类发生改变UID也不会发生改变。 | |||
对应输出流 | ObjectOutputStream | ||||
字符流
类别 | 名称 | 特点 | |||
输出流 |
|
| |||
| Writer |
| |||
对应输入流 | Reader | ||||
| OutputStreamWriter | 转换流,一般应用于在项目字符集不一致时,可以使用转换流读出,使得不会出现乱码。
| |||
对应输入流 | InputStreamReader | ||||
| FileWriter | 读取字符文件 | |||
对应输入流 | FileReader | ||||
| BufferedWriter | 是一种缓存的字符流,内部有一个8K的数组,但是不具备有操作文件的功能,类似一个大管道,传输更快。 | |||
对应输入流 | BufferedReader | ||||
| PrintWriter | 打印流,是一个字节和字符流之间的一个桥梁,可以配合System,out/in配合使用,使得输出或者输入的位置可以自定义。 常见于适用于异常 | |||
其他常用
System.out/in:标准的输出流,可以调用System.out/in来指定输出或输入位置。
Closeable接口:如果未实现这个接口的话,那么这个流则无法关闭。
AutoCloseable接口:JDK7的特性,会自动关闭流。
JDK7.0的异常新特性:在try()在()中写入要关闭的流,就可以实现自动关闭。
Properties:实现了Hashtable的类,一般用于配置文件,这个类的特点是没有泛型,而且这个类的键和值都String类型。
反射
Java类在加载时,会经历双亲委派机制,当程序主动使用某个类时,如果该类还没有被加载到内存中,则系统会通过加载、链接、初始化三个步骤对该类进行初始化,类加载是指把类的class文件读入内存中,并为之创建一个java.lang.Class对象,也就是说程序使用任何类的时候,JVM都会为其创建一个class对象。
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性(成员变量)和方法,对于任何一个对象,都能够调用它的任意方法和属性(成员变量),并且能改变它的属性(成员变量),反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括其修饰符、属性(成员变量)、方法等,并可以在运行时改变属性(成员变量)内容或者调用方法。
如此之下,就可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源码链接,降低代码的耦合度,即动态的装配。
在以前使用Spring的xml配置时,都会在spring配置文件上,配置上要扫描的包,而Spring内部就是通过反射获得这些包下的类,进而创建一个对象。
可用于判断运行时对象所属的类;对象锁具有的成员变量和方法;甚至可以调用到private方法;注意的是final是没有办法改变。
查阅Java API:java.lang.reflect.AccessibleObject
参考工厂设计模式:
/** * 工厂模式 * @author Levi */ public class J01FactoryMode { public static void main(String[] args) { /* * 工厂制造车是视市场环境来决定今年要增产哪些车型的。 * 制造哪些车,怎么制造,工厂老板并不关心。 * 并且后期要制造新的车,客户端的代码是不用直接修改的,降低了客户端代码和实现类的耦合度,传不同的参数实现不同的对象 */ Car car = Factory.getInstanceCar("BMW"); car.manufacture(); } }
/** * 车 * @author Levi */ abstract class Car { /** * 制造车 * @author Levi void */ public abstract void manufacture();
}
/** * 具体的车 * @author Levi */ class Bmw extends Car {
@Override public void manufacture() { System.out.println("开始制造BMW这款车"); System.out.println("..."); System.out.println("BMW制造完成"); } }
/** * 工厂 * @author Levi */ class Factory { public static Car getInstanceCar(String type) { if(type.equalsIgnoreCase("BMW")) { return new Bmw(); } return null; } } |
常见疑惑
java.lang.Object
Object是Java默认提供的一个类,这个类是所有类的父类,也就是说任何一个类定义的时候,没有明确的继承一个父类,默认就是Object的子类。
但继承是有子父和祖宗概念的,即所有的类最终的祖宗就是Object。
所以所有的类都默认实现了这个类的所有方法。
好处是Object是一个超类,使得语言更加灵活,可以在扩展类库时,使得其可以兼容所有类库的类。
注:接口是没有默认父类的,而且接口是用于顶层设计的,接口是不能继承类,而Object是一个类,所以接口是没有继承Object。
hashCode()
Hash翻译为散列,也有音译为哈希的,把任意长度的输入数据,如字符、字符串、数字等输入,通过算法,计算得到一个数值,这个数值就是Hash编码,即Hash是一种算法。
在Java中,对象的hashCode默认是通过对象的内存地址计算出来的一个数值,可以重写hashCode方法来编写自定义类的hash算法,默认是调用父类的hashCode方法,hashCode相当于是一个对象的编码,一般不直接调用。
其作用主要是可用于集合中的数据查找,提升查找的速度。
hashCode()返回的是该对象的哈希码值,同一个对象的哈希码值是唯一的,即不同对象,hashCode值是不同的。
equals()是返回比较的结果,默认比较的是对象的内存地址。
如果两个对象equals相同,那么这2个对象的hashCode码必须一样。
注:自定义类重写equals(),是告诉JVM如何比较这两个对象,即使得比较两个对象的时候,比较属性(成员变量)是否相等,而不是内存地址。
toString()
调用toString()和直接调用System.out.println()方法输出一个对象的时候,输出结果是一样的。
查看源码:会发现sysout会自动调用对象的toString()方法。
/** * Prints an object. The string produced by the <code>{@link * java.lang.String#valueOf(Object)}</code> method is translated into bytes * according to the platform's default character encoding, and these bytes * are written in exactly the manner of the * <code>{@link #write(int)}</code> method. * * @param obj The <code>Object</code> to be printed * @see java.lang.Object#toString() */ public void print(Object obj) { write(String.valueOf(obj)); } |
toSting()方法,默认返回对象的描述信息:
java.lang.Object@de6ced
为什么使用Set要同时重写hashCode和equals方法?
Set底层存储是根据hashCode散列计算的值来存储的,equals就是当2个hashCode值一样是用于判断内容是否相同,相同则不加入,不相同则加入。
如果2个值的hashCode相同,equals不相同,则会在其元素位置上,在开辟一个空间出来,同时存储2个元素。
即,如果只重写了equals方法,两个值的equals返回true,集合是不允许出现重复的,只能插入一个。此时,如果没有重写hashCode方法,那么久无法定位到同一个位置,集合还是会插入这个值,这样就出现了重复了,那么重写的equals方法就没有意义了,所以一定要同时同写。
注:equals相同,hashCode一定相同。相反,hashCode相同,equals不一定相同。
public class HashCodWithEquals { public static void main(String[] args) { Test t = new Test("AA", 12); Test tt = new Test("AA", 12); System.out.println(t == tt); System.out.println(t.equals(tt)); System.out.println(t.hashCode()); System.out.println(tt.hashCode()); } } class Test { private String name; private int age;
public Test() {
}
public Test(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age;
}
// 重写toString 方法 @Override public String toString() { return "name;" + name + "age;" + age; }
// 重写hashCode方法 @Override public int hashCode() { return name.hashCode() + age; }
// 重写equals 方法 @Override public boolean equals(Object o) { // 判断这个o是不是这个类的实例,不是肯定是false if (!(o instanceof Test)) return false;
// 判断这个如果是这执行一个向下转型强转 Test t = (Test) o; // 如果是判断是否有传参,数字没有空的概念 if (name == null) return false; // 判断传入的参数是不是一样 if (!name.equals(t.getName())) return false; if (age != t.getAge()) return false; return true; } } |
java.lang.String
String描述的是文本字符串序列,Java程序中看到的字符串常量值,都是String类型的对象,字符串在Java中不是基本数据类型,是引用数据类型(类类型)。
String类是final类,该类不能有子类,并且实现了java.lang.CharSequence接口。
String的字符串数据是存储在内存的常量池的,在创建N个相同的字符串时,在内存是只存储一份的,提高了内存的空间使用率。
注:new String(“abc”),这样是创建了两个对象,一个是abc这个字符常量,一个是new出来的这个String对象。
String类对equals()进行了重写:
/** * Compares this string to the specified object. The result is {@code * true} if and only if the argument is not {@code null} and is a {@code * String} object that represents the same sequence of characters as this * object. * * @param anObject * The object to compare this {@code String} against * * @return {@code true} if the given object represents a {@code String} * equivalent to this string, {@code false} otherwise * * @see #compareTo(String) * @see #equalsIgnoreCase(String) */ public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } |
Serializable接口
Seriablizable是一个序列化接口,实现这个接口后可以实现对象序列化,并且给类添加一个序列号后,可以保证在读取时,不会出现丢失,还可以保证对象的状态的一致性。
序列化
序列化的主要作用是让对象永久的存储在硬盘中。
序列化:把对象转换成二进制(字节序列)存储到文件中。
反序列化:把文件中的二进制(字节序列)对象怀府到类中就是反序列化。
注:序列化时,一般需要提供一个序列化编码,确保在恢复时,仍然是指向同一个内存区域(Serializable接口作用)。
可变序列化:对象在创建出来后,仍然可以改变位于对象中的内容,如StringBuffer。
不可变序列化:在创建对象了之后,内存中的内容是不可变的,如String。
可变类与不可变类
可变类:创建出来这个类的实例时,是可以改变这个类的实例的内容的。
不可变类:创建出这个类的实例时,是不可以改变的。
值传递与引用传递
值传递:当传递一个实际的值给这个形参时,是用来初始化这个形参的,而形参的值发生改变时,是不会改变传递过来的这个实际的值的,即没有改变传进来的这个值的内存的值。
值传递的类型是基本数据类型,即方法形参时基本数据类型,这是一个值拷贝的过程。
引用传递:当调用某个方法时,参数是对象或数组,而这个对象调用某个方法时,这个方法就是使用内存中的地址值空间实际参数进行操作的,而当这个方法结束时,这些操作修改了的结果都会保留下来,即改变了内存空间的值。
引用传递是引用类型,在传递时,是传递地址值,改变内存的数据。
public class J05Pass {
public static void main(String[] args) { J05Pass pass = new J05Pass();
int value = 6; pass.valuePass(value); System.out.println(value);
TestObject testObject = new TestObject(); testObject.setName("abc"); pass.referencePass(testObject); System.out.println(testObject.getName()); }
/** * 值传递 * @author Levi */ public void valuePass(int i) { i = 5; System.out.println("方法内修改后的值:" + i); }
/** * 引用传递 * @author Levi */ public void referencePass(TestObject testObject) { testObject.setName("ccc"); System.out.println("方法内修改后的值:" + testObject.getName()); } } class TestObject { private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
} |
可变字符序列与不可变字符序列
可变字符序列:即可改变内存中的数据,是将字符的值存储到数组中,当调用了toString()时,会把数组的值拷贝到对应的可变字符对象中(引用传递),从而实现一个追加的功能。
代表的可变字符序列类有StringBuilder和StringBuffer,在使用时,就是使用append()方法,将值存储到数组,实现这样一个追加功能。
不可变字符序列:代表类有String,String的值是存储到内存的方法区中的常量池的,并且String这个类是使用final修饰的,而且这个类底层存储是使用数组存储的,其数组也是使用final修饰的,使得其在内存中是可以改变的。
所以创建内存中没有的,一个新的字符串时,是会在内存中创建出一个新的字符串。
补充:
String:存储在常量池的。
StringBuffer:线程安全的字符类,适用于对字符串进行修改、插入、删除等操作,底层是使用数组进行存储的,默认空参的构造器数组长度是16,当超出数组长度,则会以当前长度*2+2。
StringBuilder:线程不安全的字符类,效率上优于StringBuffer,底层存储是使用数组,默认空参构造器的数组长度是16,当超出数组长度,则会以当前长度*2+2。
为什么重写Comparator方法,同时重写equals?
equals相同,hashCode必然是相同的,而重写Comparator的compare时,建议重写equals方法,可以避免2个值一样则后一个无法进来,而hashCode是确定2个值的位置。
数组和链表的区别
线性结构的典型数据关系是一对一的,是有序的数据集合,即从左往右或从右往左的行元素,而不是像二维或多维数组受行和列即更多因素的影响,这就是一对一的关系。
线性结构是除了第一个和最后一个数据元素之外,其他数据元素都是首尾相接的。
常用的线性结构有:栈、队列、数组。
特点:
集合中必须存在唯一的一个第一个元素。
集合中必须存在唯一的一个最后的元素。
除了最后元素之外,其他数据元素均有唯一的后继。
除了第一元素之外,其他数据元素具有唯一前驱。
……
即收尾相接,前后链接成线。
非线性结构的一个明显特征就是一个结点元素可能对应多个直接前驱和多个后继,各个数据元素不在保持在一个线性序列中,每个数据元素可能与零个或多个其他数据元素发生联系,这就是所谓的一对多或多对一,总之就不是一对一。同时,也会根据关系不同,可分层、分群结构。
常见的有线性结构有:多维数组、二叉树。
数组是属于线性结构,元素是可以直接索引的,链表也是线性结构的,对于元素用指针往后遍历N遍即可。
类型 | 数组 | 链表 |
内存分配 | 静态分配 系统自动申请空间 | 动态分配 手动申请空间 |
内存空间 | 连续 | 不连续 |
内存位置 | 栈区 | 堆区 |
示例 | 看电影,为了保证10个人能坐在一起,就必须提前订好10个连续的位置,这样的好处在于保证了10个人可以坐在一起,但是这样的缺点是,如果来的人不够10个人,剩下的位置就浪费了。 如果临死多来了个人,那么10个就不够用了,可能需要把第11个位置上的人挪走,或者是他们11个人重新去找一个11连坐的位置,这样的话,效率都很低。 如果没有找到符合要求的座位,就没有办法坐了。 | 在电影院中,每个人随便坐。 第一个人知道第二个人座位号,第二个人知道第三个人座位号…… 如果再来一个人了,要坐在第三个位置上,只需要找第二个人拿到原来第三个人的位置就行了,其他人不用动。 |
特点 | 数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费空间。 增加和删除数据效率低,增加数据时,这个位置后面的数据在内存中都要往后移动,因为需要连在一起。 数组是连续的,知道每一个数据的内存地址,可以直接找到对应地址的数据,因此随机读取效率很高。 但是数据定义空间不够时,要重新定义数组,所以并不利于扩展。 | 不需要一开始指定内存大小,扩展方便,数据随意增删。 在内存中可以存在任何地方,不要求连续的。 每个数据都保存了下一个数据的内存地址。 查找数据时效率低,因为不具备随机访问性,所以访问数据时,是要知道类推的方式,找到第一个,再找到第二个…… |
优点 | 随机访问性强 查找速度快(时间复杂度O(1)) | 增删速度快(时间复杂度O(1)) 大小不固定,可以动态扩展 内存利用率高,不会浪费内存 |
缺点 | 增删速度慢(时间复杂度O(n)) 内存空间要求高,必须要有足够大的连续内存存储空间 数组大小固定,不能动态扩展 | 不能随机查找,必须从第一个开始查找,查找效率低(时间复杂度O(n)) |
注:
随机读取:就是直接存取,可以通过下角标直接访问,也是数组的特性,因为数组在存储是上使用了一块连续的存储空间,元素逐个存储,可以在O(1)的时间内存,即如果知道第一个元素的位置,就可以知道第X元素的位置。
非随机读取:顺序存取,不能通过下角标访问,只能按照顺序读取,这也是链表的特性。
Hash(哈希)说明
Hash,一般翻译为散列,也有直接翻译为哈希的,就是把任意长度的输入,通过散列算法,变换固定长度的输出,该输出就是散列值,这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一确定输入值。
简单来说就是把一种把任意长度的消息压缩成某一固定长度的消息摘要的方法/函数。
哈希表也叫散列表,是一种数据结构,应用场景很丰富,有许多缓存技术的核心就是在内存中维护一张大哈希表。
数据结构的物理存储结构只有两种,顺序存储结构和链式存储结构,而上面提到过,在数组中根据下表查找某个元素,一次定位就可以找到,哈希表利用了这种特性,哈希表的主干就是数组,即通过hash方法/函数计算出hash值,进而根据这个hash值在数组查找到目标元素。
哈希冲突:世事无完美,如果有两个不同元素,通过哈希函数得出的实际存储地址相同就会出现冲突了。即当对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是所谓的哈希冲突,也叫哈希碰撞。
进而哈希方法/函数的设计至关重要,好的哈希函数会尽可能的保证,计算简答和散列地址分布均匀,但是数据是一块连续的固定长度的内存空,在好的哈希方法/函数也不能保证得到的存储地址绝对不发生冲突。
在发生哈希冲突解决方案有:开放定址(发生冲突,继续寻找下一块未被占用的存储地址)、再散列函数法、链地址法、建立公共溢出法。HashMap是采用了链地址法,也就是数组+链表的方式。
时间复杂度O(1)、O(n)、O(logn)、O(nlogn)
O(1):数据无论增长多少,查询时间依然不变,在不考虑冲突的情况下,都可以在一次计算后找到目标。
O(n):数据增大几倍,耗时就增加几倍,如冒泡排序,对n个数排序,需要扫描n x n次。
O(logn):数据增加到n倍,耗时就增加logn倍(log是以2为底,2的log次方),如数据增大到256倍,耗时只增大8倍,即在256个数据中,查找8次就可以找到目标了,典型的二分查找就是这个算法。
O(nlogn):就是n x logn,但数据增大到256倍时,耗时是256 * = 2048倍,典型的归并排序就是这个算法。
LRU算法
LRU算法的全称是Least Recently Use,即最近最少使用,是一种缓存置换算法,这个算法在很多图片加载第三方框架中都有身影存在。
这个算法的缓存机制是由LRU缓存和软引用组成的,既然是一种缓存策略,那么定不会无限制的增加,但内存达到某个阈值时,会舍弃一部分保存在内存的数据,舍弃的规则是最近最少未使用的被舍弃。
比如ABCDE,如果这个时候内存已满,需要舍弃一个,这个时候舍弃的是E,因为它最长时间没有被使用。
这个算法在实现上,可以利用LinkedHashMap的特性实现。
Comparator和Comparable的区别
特点 | Comparator | Comparable |
类型 | 外比较器 | 内比较器 |
比较对象 | 不支持自己与自己比较 | 自己与自己 |
方法 | compare(Object o1,Object o2) | compareTo(Object o) |
排序逻辑 | 在另一个实现 | 带排序的对象类中,也称为自然排序 |
触发 | Collections.sort(List,Compator) | Collections.sort(List) |
返回值 | 1、o1大于o2,返回正整数 2、o1等于o2,返回0 3、o1小于o3,返回负整数
| 1、比较者大于被比较者(也就是compareTo方法里面的对象),那么返回正整数 2、比较者等于被比较者,那么返回0 3、比较者小于被比较者,那么返回负整数
|
学生 | 按照age定制化排序 | 学号默认排序 |
小结:
一个类实现了Comaparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator接口可以看成一种算法的实现,将算法和数据分离,Comparator也可以在常见的这两个场景中使用。
一是类的设计没有考虑到比较问题,而没有实现Comparaable,通过Comparator来实现排序,而不用改变类的本身。
二是可以使用使用多种排序标准,比如升序、降序等。
个性化方面,如果实现类没有实现Comparable接口,又想对两个类进行比较,或者实现类实现了omparable接口,但是对compareTo方法内的比较算法不满意,那么可以实现Comaprator接口,自定义一个比较器,写比较算法。
解耦方面,实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改,从这个角度来说,其实也有些不太好,尤其是将实现类的.class文件打包成一个.jar文件提供给开发者使用的时候。
Fail-Fast机制
Fail-Fast机制是Java集合中的一种错误机制,但使用迭代器得带元素时,如果发现集合有修改,就会快速的抛出ConcurrentModificationException异常。
这种修改有可能是其他线程修改,也有可能是当前线程自己修改导致的,比如迭代的过程中调用remove()方法删除元素。
具体的原理可以看系列文章的ArrayList,会发现迭代器中的next()每次都会检查modCount的值,是否相同。