目录
首先声明部分图片来自北京尚学堂
首先我们可以通过类来对Java的相关知识进行记忆:
类:
属性:
属性也称之为成员变量,它从属于对象,属性用于定义该类或该类的对象的数据或静态特征,如果属性没有初始化,java默认初始化为0或空
方法:
方法就是对象的行为
重载(Overload):
重载是由方法衍生出来的,重载的意义是解放方法名的约束,可以使用相同的方法名去不同的动作,假设有一个方法特别适合某个名字,但是这个名字已经被别的方法占用了,此时就可以使用重载解决这种问题
构成重载的条件:方法名相同,形参类型不同或者形参顺序不同或形参个数不同
构造方法:
构成条件:构造方法的方法名必须与类名一致,并且无返回值
构造方法的作用就是当我们实例化对象时便于对成员变量进行赋值操作,构造方法的第一句永远是super(),如果没有编译器会自动帮你加上,当我们实例化对象时,会先调用父类的构造方法,加载父类信息然后再往下执行
this() 和 super():
而构造方法也是有重载的,也是通过相同的方法名,形参类型不同或者形参顺序不同或形参个数不同构成重载的,而构造方法之间使用this()调用,但是this()也必须在第一行,而刚刚说过super()也得在第一行,如果同时出现是谁在第一位呢?
如果有this(),就不再有super(),编译器也不会自动添加,因为this()是为了调用别的构造方法,那别的构造方法里如果没有this(),那么他就会有super(),最后终究会调用父类的构造方法加载父类信息,而为什么要有这种设定呢?
因为每一次调用super()都会加载一次父类信息,那如果不管有没有this(),第一行都是super()的话,那么假设我这个构造方法调用了另一个构造方法,但是第一行是super(),所以先加载父类信息,然后调用另一个构造方法,但是另一个构造方法第一行又是super(),所以又加载了一次父类信息,加载了两次同样的信息,这就导致了内存浪费。
面向对象:
1、封装:
封装就是对属性、方法的封装,属性建议使用private修饰,然后用get()、set()来获取或者赋值,方法建议使用public或者默认修饰,除非是辅助性方法用private
注意:
protected修饰符有一个特点,
如果在同一个包下,子类可以访问父类的成员,也可以访问父类对象的成员
如果不在同一个包下,子类可以访问父类的成员,但不可以访问父类对象的成员
例子:
我们有两个包,一个a包,一个b包
Student在a包继承了Person可以访问Person的成员,也可以访问Person对象的成员
Boy在b包继承了Person可以访问Person的成员但是不可以访问Person对象的成员
2、继承:
继承就是子类继承父类,将父类所有的属性、方法都继承过来(构造方法不行),但是不见得可以用,因为封装的原因如果父类的属性或者方法由private修饰,那么子类就无法调用
有了继承就有了super(),所有类都继承或间接继承Object类,Object类是所有类的老祖宗
3、多态:
多态就是父类引用指向子类对象,使得相同继承关系的类的对象,去调用同一函数,产生了不同的行为。比如打球,打球的子类有打篮球,打排球,打网球,这些动作都继承了父类打球,那这是不是就是相同的继承关系,同样是打球但是有不同的行为。
而说到父类引用指向子类对象就要说到:
向上转型(自动转型):
向上转型就是当我们使用父类引用指向子类对象时,系统会默认向上转型
向下转型(强制转型):
当我们想要使用子类特有的方法时,需要向下转型
因为有一句口诀叫:编译时看左边,运行时看右边
编译时编译器只认左边,那么父类引用指向子类对象,编译阶段只认父类,如果使用子类特有的方法,但是父类没有啊,所以如果父类引用指向子类对象时想要使用子类特有的方法,需要向下转型
向下转型会遇到什么风险:
ClassCastExceptin(类型转换异常)
如何避免这种风险:
使用instanceof关键字,判断该类是否为某个类的示例、某个类的子类示例、某个接口的实现
重写:
说到多态那么就有了重写,当子类继承了父类就可以重新父类的方法,重写提高了代码的扩展性
重写有一个口诀:
两同两小一大
两同:参数列表和方法名相同
两小:子类抛出的异常小于等于父类
子类的返回值小于等于父类
一大:子类的访问权限修饰符大于等于父类
创建对象内存分析:
创建对象内存分析我写成了另一个博客:
创建对象内存分析https://blog.csdn.net/cccccccmmm/article/details/125940147
抽象类abstract:
1、有抽象方法的类只能定义成抽象类
2、抽象类不能被实例化
3、抽象类可以包含属性、方法、构造方法。但是构造方法不能用new实例化,只能用来被子类调用
4、抽象类就是为了用来被继承的,所以不能加final修饰
5、抽象方法必须被子类实现
接口interface:
接口比抽象类更加抽象,他里面只有常量和抽象方法,接口为了规范实现它的类的
定义接口的详细说明:
访问修饰符:只能是公开或者默认的,接口就是用来被实现,规范类的,如果是私有的那还要接口干嘛?
extends:接口可以多继承
常量:接口中的属性只能是常量,总是public static final修饰
抽象方法:接口中的方法只能是抽象方法,总是public abstract修饰
接口中的默认方法可以有方法体,只要我们在定义方法的时候方法名前面有关键字default这个方法就可以有方法体
接口中的静态方法也可以有方法体,但是他只能用接口去调用,如果子类实现了接口,子类也写了一个和接口相同的静态方法,那么这两个是不同的两个方法,一个用接口调用,一个用类名调用
内部类:
内部类顾名思义就是包含在外部类中的类,就叫内部类,内部类分为静态内部类和非静态内部类
非静态内部类:非静态内部类相当于类的属性,它是从属于对象的
静态内部类:静态内部类相当于静态方法,它是从属于类的
实例化的不同:
1、非静态内部类实例化时需要先实例化外部类然后实例化内部类
new 外部类().内部类()
2、而静态内部类不需要创建外部类
new 外部类.内部类()
通常为了方便我们都会使用静态内部类
匿名内部类:
匿名内部类比较特殊,他可以用来实例化接口和抽象类,接口和抽象类不是不能实例化吗,但是我们可以通过匿名内部类实现类似实例化的效果,我们通过代码来看看:
通过该代码我们可以看到我们实例化了接口,但是后面带了个大括号,大括号里实现了接口的方法
数组:
一维数组:
1、一维数组的声明:
数组有两种声明的写法:
int[] arr;
int arr[];
注意声明时不能给定数组大小,只有在创建时才给定数组大小
2、一维数组的创建(给数组分配空间):
数组只有在创建时才会分配空间
int[] arr = new int[];
3、数组初始化:
3.1、静态初始化(数组在声明时使用大括号初始化):
int[] arr = {0,1,2,3,4};
3.2、动态初始化(数组声明创建过后,通过下标初始化):
int[] arr = new int[3];
arr[0] = 0;arr[1] = 0;arr[2] = 0;
3.3、默认初始化(数组声明创建过后,系统默认初始化为0或空)
int[] arr = new int[3];
4、一维数组创建的内存分析:
int[] arr = new int[3];
数组实际上也是对象,当我们new的时候其实跟对象创建四部原则差不多:
1、创建对象,属性默认初始化为0或空
2、属性进行显性初始化
3、调用构造器
4、返回对象的地址给变量
二维数组:
二维数组声明:
int[][] arr;
二维数组的创建:
int[][] arr = new int[3][]//创建数组时前面这个括号必须定义大小
二维数组的初始化:
静态初始化:int[][] arr = {{1,2,3},{4,5,6}};
动态初始化参考一维数组
默认初始化:int[][] arr = new int[3][]//创建数组时前面这个括号必须定义大小
创建二维数组的内存分析:
数组也是对象,创建数组与创建对象四步法一致:
我们以这段代码模拟创建数组的内存分析int[][] arr = new int[3][2];
注意:
二维数组在创建过后new int[3][],像这样只有前面的括号有数字,后面的括号是空的,那么系统就不会自动创建一维数组给二维数组的元素引用,此时如果你访问某个值那就会报空指针异常
因为当我们创建二维数组时如果后面的括号不给定大小,那么java会默认赋空值
如果我们在后面的括号定义了数字,二维数组创建时就会默认创建三个一维数组给二维数组的元素引用,并且每个一维数组的元素默认为2个(这里的一维数组的元素默认是你自己定的)
异常:
Exception是所有异常类的父类,其子类对应了各种各样个可能出现的异常事件。通常JAVA的异常可分为:
RuntimeException运行时异常(运行时编译器才会报错)
CheckedException编译时异常(编译时编译器就会报错)
对异常的处理方式:
第一种方式:在方法声明的位置上,使用throws关键字(抛给上一级)(声明式异常)
异常抛出又有两种:
第一种:直接抛出异常
第二种:异常也有父类,可以抛出父类异常
抛出的特点:只要发生了异常,那么此方法的后续代码不会执行
第二种方式:使用try…catch语句进行异常的捕捉
try的特点:try语句块中某一行出现异常,那么这行后面也不会执行
catch的特点:catch可以写多个,catch对于类型的捕捉必须
catch异常从上到下,从小到大,先处理子类异常再处理父类异常
包装:
Java是一个面向对象的语言,但是他并不是一个纯面向对象的语言,因为基本数据类型就不是对象,但是大部分操作都是需要通过对象来处理的,作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化,所以java提供了包装类
为了让包装类和基本数据类型一样方便好用,所以java提供了自动装箱和自动拆箱操作
Integer x = 100; 等于 Integer x = Integer.valueOf(100)
int i=x; 等于 int i = x.intValue();
自动装箱和拆箱本质上就是编译器自动帮我们加上valueOf方法和Value方法
缓存机制:
valueOf方法里边有一个缓存机制
包装类(装箱)引用数据类型在加载的时候会初始化整数型常量池(-128~127)
只要装箱的范围在-128~127之间,那么就不会new对象
我们可以测试一下:
通过一下代码我们会发现,值为1的Integer对象的地址是相同的,值为128的对象的地址是不同的,==判断的就是两个对象的地址嘛,这足以证明当我们在加载包装类时会初始化整数型常量池(-128~127)
我们也可以通过源码判断:
valueOf这个方法在最开始会判断传入的值是否为大于等于low并且小于等于high的,如果是的话返回某个数组中的值
而low就是-128,而high就是127
泛型:
泛型的本质:数据类型的参数化
好处:
1、代码可读性更好
2、程序更加安全(只要编译时期没有报错,就不会出现ClassCastException)
语法结构:
Public class 类名<泛型标识符号>{…}
Public class 类名<泛型标识符号,泛型标识符号>{…}
如果属性、方法想要使用泛型那么定义类的时候就必须指定泛型,(如果只是部分方法要使用泛型可以用泛型方法而类不定义泛型)
就是说我下面要用到什么泛型,就得在类定义的时候使用<>列出来
泛型类:
泛型用于类的定义中,被称为泛型类,在实例化泛型类时,必须指定T的具体类型
泛型接口:
泛型接口于泛型类的定义及使用基本相同
泛型方法:
当我们需要大规模使用泛型时,可以在定义类时定义泛型
如果我们只需要部分非静态方法使用泛型,那么就可以使用泛型方法
非静态方法语法结构(非静态方法可以直接访问类定义的泛型):
无返回值方法
public<泛型标识符> void getName(泛型标识符 name){…}
有返回值方法
public <泛型标识符> 泛型标识符 getName(泛型标识符 name){…}
静态方法不能使用类定义的泛型,所以必须在方法中定义泛型
<泛型标识符号>尖括号括起来的叫定义泛型
泛型标识符——直接使用的叫运用泛型
容器:
容器就是用来容纳数据或者管理数据的,数组就是一种容器,他可以在其中放置对象或者基本数据类型。
数组的优势:是一种简单的线性序列,可以快速的访问数组元素,通过下标,效率高,如果从检索效率和类型检查来看,数组是最好的
数组的劣势:
1、数组的存储是连续的,所以我们需要一块连续的空间,当我们需要存储很大的数据时,就无法使用数组
2、不灵活,需要实现定义好大小,不能随着需求的变化而扩容
所以基于数组并不能满足我们的需求,我们需要更强大、更灵活、容量随时可扩充的容器来装载我们的数据——容器,也叫集合(Collection)、
Collection接口:
List接口(有序可重复):
ArrayList容器:
ArrayList容器就是数组,数组查询效率高,增删效率低,因为数组是连续的,那么如果增加或者删除就需要先将后面的所有元素进行一次移动,所以效率较低
ArrayList是List接口的实现类,拥有List的特点:
有序:怎么放进去就怎么拿出来,List每个元素都有索引,可以通过索引访问每个元素,就像数组一样,从而精确控制元素
可重复性:有了下标,就不用怕存储值重复,但是无法辨别了
ArrayList底层使用数组实现的存储
ArrayList的三种实现方式:
1、ArrayList list = new ArrayList();
好处:方法丰富,不仅有Collection接口的方法还有List接口的方法,还有ArrayList自己的方法
坏处:如果我们想要换容器会很麻烦,因为用了ArrayList特有的方法,如果换了容器,别的容器没有ArrayList方法,就需要修改
2、List list = new ArrayList();
好处:换容器较为方便,如果换List接口的容器,就非常的方便,就换个容器名就行了
坏处:方法没有ArrayList类那么丰富
3、Collection list = new ArrayList();
好处:换容器方便
坏处:方法稀少
LinkedList容器:
LinkedList容器底层是是用双向链表实现的,特点是:查询效率低,增删效率高。因为双项链表内存不是连续的,是根据一个节点连着一个节点的,当我们增加数据时,只能在结尾添加数据或在头部添加数据,并不会涉及到数组那样数据前移后移的问题。而删除数据就直接将该节点删除就行了,所以链表的增删效率高
我们可以通过底层原码分析对链表增删数据进行深度理解:
JAVA容器LinkedList底层源码分析①https://blog.csdn.net/cccccccmmm/article/details/126035702
为什么查询效率低呢,这是因为双项链表查询只能从头部往后查询或者从尾部往前查询
我们可以通过底层原码分析对链表查询进行深度理解:JAVA容器LinkedList的get()源码分析https://blog.csdn.net/cccccccmmm/article/details/126044322
Set接口:
Set接口继承了Collection接口,Set接口中没有新增的方法,它和Collection接口保持完全一致。
Set接口的特点是无序不可重复,并且Set接口下的容器没有下标,正是因为没有下标所以无序不可重复,因为下标可以更准确的确定某个数据的位置,并不用担心元素重复的问题。
无序:Set是没有下标的,我们只能遍历查找
不可重复:不允许加入重复的元素。更确切的说,新元素如果和Set接口中某个元素通过equals()对比为true,则只能保留一个
常用容器:
HashSet容器:
HashSet是Set接口的存储特征的实例化,
HashSet没有提供指定位置的方法,因为他是无序的
特点:无序、不可重复、线程不安全
无序:在HashSet底层是使用HashMap存储元素。HashMap底层使用的是数组与链表实现元素的存储。元素在数组中存放时,并不是有序存放的也不是无序存放的,而是对元素的哈希表值进行运算决定元素在数组中的位置
不可重复:当两个元素的哈希值相同时,就会在数组的同一个位置下存放,会调用equals方法判断两个元素是否相同,如果相同则不添加该元素。如果不同则会使用单向链表的方式在数组中存储
大概内存模型是这样的:
TreeSet容器:
TreeSee底层是由TreeMap存储元素的,TreeMap存储数据时会对元素进行排序,我们需要给定比较规则,如果存储的是自定义元素我们有两种规则实现的方式:
1、通过实现Comparable接口然后实现接口下的compareTo方法进行比较
2、通过外部比较器实现规则
自定义元素需要重写equals方法,因为不可重复的原因当我们通过比较过后,可能会有相同的元素那么就会调用equals方法判断对象之间是否相等
Map接口:
说了HastSet容器和TreeSet容器,大家可能会疑问,为什么HastSet和TreeSet都是以Map存储元素的,HastSet以HashMap存储元素,TreeSet以TreeMap存储元素,那么我们就来讲讲HashMap:
HashMap我们通过底层原码分析对哈希结构进行深度理解:Java容器HashMap底层分析_cccccccmmm的博客-CSDN博客https://blog.csdn.net/cccccccmmm/article/details/126064010
不过这里还是建议如果想要更加理解容器,还是需要手写容器
单向链表和双向链表的实现
Java线性结构的实现https://blog.csdn.net/cccccccmmm/article/details/126254856
Java数据结构实现二叉树
Java数据结构实现二叉树https://blog.csdn.net/cccccccmmm/article/details/126268507
IO流:
IO流我们可以通过流的分类来记忆:
按照流的方向:
输入流:数据流向是数据源到程序(以InputStream,Reader结尾的流)
输出流:数据流向是程序到目的地(以OutputStream,Writer结尾的流)
按照流处理数据的单位:
字节流:以字节为单位获取数据,命名以Stream结尾的流一般都是字节流
字符流:以字符为单位获取数据,命名以Reader/Writer结尾的流一般都是字符流
按照流处理的对象:
节点流:直接操作数据的流就是节点流
处理流:不是直接操作数据的流就是处理流
写法:
不管是什么流都是需要关闭的,所以有了try...catch...finally语法
通过try...catch...finally对流进行操作与关闭
但是大家也看到了,通过这种方法操作IO流会发现,程序非常的臃肿,整段代码的核心只有一个就是这一段
那么我们该如何进行改良呢,JDK1.7发布了新的语法try-with-resource,只要实现了java.util.Autocloseable接口,在此接口下有一个close方法,该方法用于关闭资源,只要实现了这个接口的对象,都可以使用try-with-resource自动关闭流:
总结:通过JDK1.7的新语法try-with-resource就可以避免臃肿的代码,程序可读性更好,但是这里要注意,try-with-resource只是个语法糖,当编译器编译过后,还是会把try-with-resoure这种写法转变为try...catch...finally。
多线程:
运行方式:
串行:一个CPU上顺序执行任务
并行:任务数小于等于CPU核数,即任务真的是一起执行的
并发:CPU采用时间片管理的方式,交替的处理多个任务,当任务数大于核数时,会使用并发,使得看起来像是任务一起执行,其实只是因为切换的太快,看上去像是一起执行而已
如何实例化:
通过继承Thread类:
在java中通过java.lang.Thread类来实现多线程
但是通过继承Thread类有一种缺陷,因为Java只有单继承,所以说如果该类已经继承了别的类那么该类就无法继承Thread类实现多线程
- 通过实例化Thread来创建新的线程
- 每个线程都是通过某个特定的Thread对象所对应的run()来完成其操作的,
- 通过调用start()方法实现线程的启动(调用run方法不会启动此线程,只是普通的调用方法)
通过实现Runnable接口:
为了解决单继承无法继承Thread类的问题,我们还可以通过实现Runnable接口来实现多线程:
但是Runnable接口只有一个run()方法,上面我们说过了想要启动线程就必须调用线程的start()方法,所以我们需要创建Thread对象,Thread类有一个构造方法是传递需要实现了Runnable接口的对象的,然后我们在通过这种Thread对象包装实现了Runnable接口的对象来调用start()方法启动线程
线程的声明周期:
一个线程在它的声明周期内,需要经历五个状态:
1、新生状态
用new关键字创建了一个线程对象后,该线程就处于新生状态,处于新生状态的线程有自己的内存空间,通过调用线程对象的start()方法进入就绪状态
2、就绪状态
处于就绪状态的线程拥有争夺CPU时间片的权利,等待CPU的调度,就绪状态不是执行状态,一旦获得CPU时间片线程默认调用run方法,有四种原因会导致就绪状态:
2.1:新生状态:线程调用start()方法
2.2:运行状态:CPU分配的时间片用完,返回就绪状态继续等待CPU的调度
2.3:阻塞状态:阻塞解除,进入就绪状态
2.4:运行状态:调用yield()方法线程让步,返回就绪状态等待CPU调度
3、运行状态
当线程分配到CPU时间片时,线程自动调用run()方法,如果给定的时间片消耗完,返回就绪状态继续等待,也可能由于某些原因导致阻塞状态
4、阻塞状态
阻塞状态指的是暂停一个线程,等待某个条件达到,返回就绪状态
5、死亡状态
当一个线程的run方法执行完毕即为死亡状态
线程的使用:
线程休眠:通过调用线程对象的sleep()方法来完成线程的休眠,sleep方法可直接让运行状态的线程转为阻塞状态,等待睡眠结束返回就绪状态抢夺CPU时间片
sleep方法需要传值,参数单位为毫秒。
线程让步:
通过调用yield()方法可将运行状态的线程直接转为就绪状态,但是yield()方法通常无法达到让步的目的,因为调用了yield方法会让线程转为就绪状态抢夺CPU的时间片,但是此线程可能又抢到了CPU时间片,这里通过一张图来解释:
这张图表示线程的运行内容 :
此时我们可以看到while下面的第一句,当i为2时且此时为B线程对象那么让B线程让步,此时如果我们通过启动两个线程对象来看
这张图表示线程的创建以及死亡:
当我启动了两个线程对象时,我们会发现B对象抢夺的时间片比A多,这就说明了让步通常无法达到让步的目的
线程联合:
通过join方法将两个线程联合,join方法会让并发的线程转为串行,联合对象需要等待被联合对象执行结束后,才能继续执行,就像java中的方法一样
我们通过图来讲解:
首先我们创建AThread线程,AThread线程启动BThread线程后,当i==2时A线程与B线程联合,AThread会输出的名字和i
然后我们来看BThread线程,BThread会输出自己的名字和i
我们通过主线程来调用,启动a线程
运行结果是这样的:
总结:联合线程需要等待被联合线程执行结束才能执行
要点:只有线程启动后才能被联合,联合过后的线程转为串行运行
线程是否存活:
通过调用isAlive()方法来判断当前线程为活动状态,活动状态指的是:线程已经被启动且尚未终止,意思就是线程处于就绪状态或者运行状态、阻塞状态就认为线程存活,返回布尔值
线程优先级:
每一个线程都是有优先级的,线程创建后默认优先级为5,优先级范围1~10,从小到大,从低到高
注意:线程优先级并不代表线程优先执行,只是可能被CPU调度的概率更高
int getPriority();//获得线程当前优先级
void setPriority(int newPriority)//设置线程优先级
守护线程:
java线程有两种:
用户线程:就是应用程序里的自定义线程
守护线程:守护线程是一个服务线程,它可以服务任何线程
守护线程的特点:被守护线程死亡,守护线程跟着死亡,相当于寄生关系
通过线程对象.setDaemon(boolean on)来设置线程为守护线程,默认为用户线程false,如需设置为守护线程传递true
要点:在哪个线程对象调用 线程对象.setDaemon(boolean on),那么该守护线程就守护哪个线程
多线程的同步:
多线程的同步https://blog.csdn.net/cccccccmmm/article/details/126165387
TCP/IP:
基于TCP协议通信:
TCP三次握手(可靠性连接协议):面向连接
TCP必须两端都建立联系才能发送数据
基于UDP协议通信:
UDP协议(非可靠连接):UDP不需要双方都建立联系
面向无连接
UDP可能会数据包丢失,因为不会考虑网络原因
TCP通信和UDP通信的区别:
TCP和UDP区别:
TCP单向通信实现:
Java客户端和服务端连接与单项通信https://blog.csdn.net/cccccccmmm/article/details/126185476
TCP双向通信以及优化实现:
JavaTCP通信(双项通信、一对一通信、一对多)https://blog.csdn.net/cccccccmmm/article/details/126200994
反射机制:
Java反射机制https://blog.csdn.net/cccccccmmm/article/details/126218152
Lambda表达式:
Java的Lambda表达式https://blog.csdn.net/cccccccmmm/article/details/126234818