oo-java
此文章为作者基于浙江大学mooc中的java基础和面向对象的相关学习而写,
参考书籍《Java编程思想》
一切内容均来自于个人,以下所谓“差异”,均比较与C语言
Java基础
Java中的大部分内容和c语言极其相似,以下列出一些不同点
基本数据类型差异
整数类型 | byte | 一个字节 |
---|---|---|
short | 两个字节 | |
int | 四个字节 | |
long | 八个字节 | |
浮点类型 | float与double | 四字节与八字节(同c语言) |
字符类型 | char | 两个字节,‘\u0000’~‘\uffff’,支持汉字 |
布尔类型 | boolean | 一个字节,true false |
Java也具有null
类型
字面值常量差异
0
开头表示八进制
0B/0b
表示二进制
0X/0x
表示十六进制
同时支持_
分隔数字,便于分清数字位数(类似于python)
字面值常量整数默认为int
,长整型long
在数字末尾需要添加L
字面值常量浮点数默认为double
,浮点型float
在数字末尾需要添加F或f
boolean
为逻辑数字类型,只有true和false
两种字面值,并且不对应于任何整数值1和0(这一点与c语言不同)
变量声明差异
大部分和c语言一样,需要提前声明变量,并且需要进行初始化
java的变量如果不进行初始化,则会默认设为0或者false
Java11后支持局部变量类型推定,使用var
进行声明变量,变量类型未定,根据上下文自动推定类型
运算提醒
与c语言相同,对于&&
和||
运算都具有“短路”现象,只有左操作数无法决定最终的值时才会计算右操作数
>>>
代表无符号右移,高位补0
instanceof
测试某个对象是否为指定类的对象实例,返回boolean,类似于python
整数计算的时候默认为int
型,即使所有参与运算的数据类型都低于int
,除非有long,则上升类型为long
带有浮点数运算默认为float
型,除非有double,则上升类型为double
控制语句差异
除了c语言一样的控制语句之外,java还有一些特殊的控制语句形式
1 | static void rangeOf(int k){ |
支持多种条件写在同一行,同时不需要冒号,使用->
,不需要break语句跳出(在JDK14版本之后)
增强型for循环,主要用于枚举、数组和集合对象的元素遍历
Foreach用法,用于数组和容器
for(<类型><变量>:<集合对象>)
声明类型需要与集合对象相兼容
1 | public class Main { |
移位操作符
移位仅针对int
,long
使用,对于byte, char, short
,在移位前都会转换成同数字大小的int
位操作都是针对二进制补码操作
<<
左移,低位补0
>>
有符号右移,如果是正数,高位全部补0,如果是负数,高位全部补1
>>>
无符号右移,不论正负,全部补0
java对于移位操作的数值有一定限制,对于int类型的操作数只考虑低五位,long类型只考虑低六位
比如说30>>32
等价于30>>0
,有一个类似于取模32的过程,对于long就有个取模64的过程
static详解
static大多数用于类的成员变量或者成员方法,少数用于代码块提升性能,static不能修饰局部变量
- static修饰的成员变量或者方法属于类,因此生命周期和类相同
- 普通成员变量或者方法属于对象
- 静态方法不能调用非静态成员变量,会报错
- 非静态方法可以调用非静态方法和静态方法,可以调用非静态成员和静态成员
static方法称为静态方法,不依赖于任何对象就能使用,因此也不具备this的使用,其不依赖于任何对象,没有对象,也就没有this
静态方法中不能使用非静态成员变量和非静态方法,因为后者都是需要依附于对象才能被调用
1 | public class Main { |
这个示例的第17行,在静态的方法内调用了非静态成员变量address
,系统报错
第18行,在静态方法内部调用了非静态方法test1()
,系统报错
但是对于非静态方法,其可以随意调用静态变量和方法
静态方法可以不创建对象就调用,最常见的就是main方法是静态的
- 静态变量被所有对象共享,内存只有一份,不同对象都可以修改该静态变量
- 非静态变量相互独立,在对象创建的时候进行初始化,有多个副本互不影响
在类被加载的时候,static代码块就按照顺序进行初始化了
静态的加载顺序是从上往下依次执行,静态变量和静态代码块视作同一类,从上往下执行
静态代码块
静态代码块里面定义的变量都是局部变量,只在本块中有效
初始化与清理
构造器初始化
在类里面使用和类名相同的方法作为构造函数,可以有多个构造函数,区别在于参数不同
构造器没有返回值
如果类没有构造器,系统会自动为你创造一个默认构造器
方法重载
同一个类里面支持多个同名方法,区别在于参数不同,方法重载在涉及到数据的类型转换时可能有一些问题
传入的参数类型低于要求的类型,就会自动提升参数类型
传入的参数类型高于要求的类型,就会自动降低到参数对应的类型
this
this表示对当前对象的引用,可以作为返回值,返回对当前对象的引用
构造器之间可以相互调用,但是一次只能调用一个构造器,使用this调用
只有构造器能调用构造器,其它方法不能调用构造器
变量初始化顺序
类的内部,变量的初始化顺序取决于其定义的先后顺序,但是不论他们处于任何位置,他们都会在构造器和任何方法调用之前完成初始化
多个类
一个.java
文件允许有多个class
,但是一般只有一个class
由public
修饰作为主类,主类的名字和.java
文件名字相同
类与类的关系
- 关联:是一种has的关系
- 一对一
- 一对多
- 依赖:比has关系较弱,类之间的调用关系
- 聚集:整体和部分之间的联系
- 组合
- 泛化:类之间的继承关系
- 实现:类与接口的关系
封装
- 将对象的属性和方法看成一个整体
- 就信息进行隐藏,赋予一定的权限
一般情况下,给所有的成员变量加入private
权限限制,写相关属性的get和set
方法,将这些方法设置为public
,从而通过方法访问私有属性
private
仅仅在本类中可见,提升数据的安全性public
权限最低,其它类中也可以访问,减少代码冗余protected
向子类和同一个包中的类公开- 默认权限,向同一个包中的类公开
protected
修饰规则详解:
- 基类(父类)的protected成员(包括成员变量和成员方法)对本包内可见,并且对子类可见;
- 若子类与基类(父类)不在同一包中,那么在子类中,只有子类实例可以访问其从基类继承而来的protected方法,而在子类中不能访问基类实例(对象)(所调用)的protected方法。
- 不论是否在一个包内,父类中可以访问子类实例(对象)继承的父类protected修饰的方法。(子父类访问权限特点:父类访问域大于子类)
- 若子类与基类(父类)不在同一包中,子类只能在自己的类(域)中访问父类继承而来的protected成员,无法访问别的子类实例(即便同父类的亲兄弟)所继承的protected修饰的方法。
- 若子类与基类(父类)不在同一包中,父类中不可以使用子类实例调用(父类中没有)子类中特有的(自己的)protected修饰的成员。(毕竟没有满足同一包内和继承获得protected成员的关系)
封装的单实例模式
“懒汉式”单例
1 | public class Singleton { |
• 是否 Lazy 初始化:是,只有需要的时候才会初始化
• 是否多线程安全:否
• 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
• 特点这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作只有一个实例,共享的数据,便于频繁查询修改
“饿汉式”单例
1 | public class Singleton { |
• 是否 Lazy 初始化:否,不论需不需要,类加载的时候就直接创建实例初始化
• 是否多线程安全:是
• 优点:没有加锁,执行效率会提高。
• 缺点:类加载时就初始化,容易产生垃圾对象,浪费内存。
• 特点:它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化。
常用集合框架设计
List列表
有序性,可重复性
列表的三种类型,E为每个元素的类型
List<E> list = new ArrayList<>()
,基于数组实现的动态数组,增删慢,查询快List<E> list = new LinkedList<>()
,基于链表实现的双向链表,增删快,查询慢List<E> list = new Vector<>()
,类似于ArrayList
,多线程安全,性能较差
常用方法:
list.size()
,返回元素个数list.add({int index ,}E element)
,可选参数index
,默认在列表尾部添加元素list.get(int index)
,获取对应下标的元素list.set(int index, E element)
,设置对应下标的值list.remove(int index)
,移除指定下标的元素list.isEmpty()
,检查列表是否为空list.contains(Object o)
,是否包含某元素list.indexOf(Object o)
,某个元素第一次出现的下标list.subList(int fromIndex,int toIndex)
,获取子列表- 待补充
列表初始化
在已知列表即将被初始化后的每个元素类型时,使用一些已有的来构造列表
获取已有的列表的元素List<String> newList = new ArrayList<>(existingList)
比如常用的List<Map.Entry<String, Name>> list = new ArrayList<>(map.entrySet())
map.entrySet()
就是返回一个由键值对构成的集合
Map键值对
无序性,键的不可重复性
四种类型,E1
为键的类型,E2
为值的类型
Map<E1, E2> map = new HashMap<>()
,基于哈希表实现,提供快速查找,插入,删除Map<E1, E2> map = new TreeMap<>()
,基于红黑树实现,提供有序键值对,按照键的自然顺序或自定义顺序排序Map<E1, E2> map = new LinkedHashMap<>()
,基于哈希表和链表实现,保持了插入顺序或者访问顺序,允许迭代时按顺序访问- 待添加
常用方法:
map.put(E1 key, E2 value)
,向表中添加键值对map.containsKey(E1 key)
,查询是否包含“键”map.get(E1 key)
,返回对应键的值map.remove(E1 key)
,删除对应键的值map.size()
,获取map的大小map.isEmpty()
,检查map是否为空map.keySet()
,获取map的键的集合map.values()
,获取map的值的集合map.entrySet()
,获取map的键值对的集合,常常用于返回给列表进行排序map.clear()
,清空所有键值对- 待添加
HashSet集合
实现了Set
接口,存储一组唯一元素,不允许元素的重复性,无序,基于哈希表实现
hashSet.add(E e)
,添加元素,如果已经存在,不会重复添加hashSet.remove(E e)
,移除元素hashSet.contains(E e)
,检查是否包含hashSet.size()
,元素数量,集合大小hashSet.isEmpty()
是否为空hashSet.clear()
,清空集合
Iterator迭代器
Iterator
是一个接口,用于遍历集合(如List
, Set
, Map
)中的元素。
Iterator
提供了标准的迭代方式,不用知道底层逻辑,允许顺序访问集合中的元素
调用集合的iterator()
方法获取实例
1 | List<String> list = new ArrayList<>(); |
由于Java5之后有增强型的for-each,显式的iterator不再使用
继承
父类的属性和方法可以应用于子类,代码复用性高,可以轻松自定义子类
- 父类superclass,子类subclass
- 单继承与多继承,多继承拥有多个父类
抽象类
1 | abstract class Animal { |
子类拥有父类所有的属性和方法,包括私有属性,但是无法直接访问私有属性,需要父类的get()方法间接访问
子类无法继承父类的构造方法,构造方法无法被继承,子类需要定义自身的构造方法
- java不支持类的多继承,但是支持接口的多继承,一个类只能有一个直接的父类,使用
extends
连接 - 子类调用父类都需要super(),super().方法名
- 将基类数据成员定义为private,使用相应的构造函数去访问,保证封装性,降低效率
- 基类仅仅对派生类提供服务时,可以设置为protected,如果有除了派生类以外的无关类,则建议private
- 派生类可以自动向基类进行类型转换,向上映射会丢失子类的独有的方法,但是如果是继承的方法,那么调用时依然调用子类重写的方法
运行时类型:由该变量指向的对象类型决定
编译时类型:由该变量声明时的类型决定
覆盖:子类重写父类的方法,方法名和参数类型完全一样,覆盖是对于实例方法而言的
方法不能交叉覆盖:子类实例方法不能覆盖父类静态方法,子类静态方法也不能覆盖父类实例方法
隐藏:父类和子类具有相同的名字的属性或者方法(方法隐藏仅有一种可能,父类和子类都是静态方法),父类的同名属性或者方法在形式上不见了,实际还是存在的
- 当发生隐藏时,声明类型是什么类,就调用对应类的属性或者方法,而不会有动态绑定(运行时)
- 属性只能被隐藏,无法被覆盖
- 变量允许交叉隐藏,即非静态的隐藏静态的,静态的隐藏非静态的
隐藏与覆盖的区别:
- 被隐藏的属性,在子类向上转型成父类时后,访问的是父类中的属性,
- 在没有转换时,子类访问父类的属性需要super关键字
- 被覆盖的方法,在子类向上转型成父类后,调用的还是子类自身的方法,如果想要访问父类还是可以用super关键字
1 | public class Test { |
output
1 | shape constructor |
final用法
- final修饰变量,final变量被赋了初值就无法再改变
- final修饰的方法不可被重写,比如某个父类的方法加入final,那么子类无法再重写该方法
- final修饰的类无法被继承
多态
静多态:编译时多态,静态联编,静绑定。方法重载,方法隐藏
方法重载:方法同名,参数列表不同
方法重载特例:允许不同访问权限修饰,允许不同返回值,构造方法和静态方法也可以重载
动多态:运行时多态,动态联编,动绑定。条件:继承,覆盖,向上转型
有继承的情况,继承中必须有方法覆盖(override),通过父类调用被覆盖的方法
方法覆盖:方法名,参数列表一致,子类方法不能缩小访问权限
方法覆盖特例:私有方法,静态方法不能被覆盖,如果子类有同名方法,那就是执行方法隐藏(实际还是存在),final方法不允许覆盖
抽象
关键字abstract
修饰方法和类,表示“尚未实现”
抽象类:通常作为其它类的super类,父类
抽象方法:没有方法体,直接以分号结束,抽象类可以没有抽象方法,有抽象方法必须为抽象类
抽象类的引用:抽象类虽然不能实例化,但是可以引用,作为向上转型的桥梁,将子类实例传递给抽象类的引用实现多态
抽象方法的权限:抽象方法不能被private, final, static
修饰,因为需要重写覆盖(override),还有访问相关的问题
接口
Interface
关键字说明“接口”,接口中可以定义常量和方法,都是默认public abstract
的
接口和类的区别:
- 接口不能用于实例化对象
- 接口没有构造方法
- 接口所有的方法必须是
public abstract
,一般建议写上,不写也是默认这种 - 接口没有成员变量,其变量都是
public static final
的 - 接口需要被类实现
- 接口支持多继承,也就是一个类允许实现多个接口
- JDK1.8之后,接口允许包含“默认方法”,使用default修饰
- 接口之间允许继承,越继承该接口需要实现的方法一般就越多
class 类名 implements 接口1[,接口2,接口3…]
- 在“实现类”实现接口时,类要实现接口中的所有方法,否则必须声明为抽象类
- 类在实现/重写接口的方法时,要保持一致的方法名和参数,返回值可以选择兼容的,权限修饰必须为public,不能缩小修饰范围,否则编译报错
- 一个类继承多个接口时,多个接口之间的同名方法的返回值需要互相兼容,否则无法同时实现多个接口中的方法,造成编译报错
- 一个类继承多个接口时,多个接口之间的变量不要同名,否则在实现类里面调用接口的常量时,编译器无法确定调用的是哪一个接口的常量。当然也可以在调用常量的时候说明调用的是哪一个接口的常量
内部类
成员内部类
成员内部类是在一个类的内部定义的类,它与外部类之间有特殊关系,因为内部类实例依赖于外部类的实例
- 定义方式:成员内部类是在外部类中定义的,可以像定义其它成员变量一样定义内部类
- 访问:成员内部类可以访问外部类的成员,包括私有成员
- 初始化:成员内部类的实例通常需要外部类的实例来初始化
1 | public class OuterClass { |
局部内部类
局部内部类是在方法内定义的类,它的作用域仅限于它的方法。局部内部类通常用于需要实现某个接口或者继承某个类的情况
1 | public class OuterClass { |
匿名内部类
匿名内部类是一种没有显式名称的内部类,通常用于创建临时对象,实现接口或者继承类的情况。通常在创建对象的地方创建,并且可以包含类的定义和初始化。
1 | interface MyInterface { |
内部类的初始化过程: 内部类的初始化与外部类的初始化过程是相互独立的。在初始化内部类之前,外部类必须首先初始化。内部类的初始化可以在需要时进行,它不会随着外部类的加载而立即发生。
内部类的调用情况: 内部类可以通过外部类的实例来访问,也可以通过创建内部类的实例来访问。外部类可以访问内部类的成员,前提是内部类的成员具有适当的可见性修饰符(例如,public
、protected
、default
或 private
)。
内部类的多重嵌套: Java支持内部类的多重嵌套,即一个内部类可以包含另一个内部类,可以嵌套多层次。这种多重嵌套通常用于创建复杂的数据结构或实现某些设计模式。
1 | public class OuterClass { |
还可以为内部类写一个私有的返回外部类对象的方法
1 | private xxxxxxx(外部类名字) getOuterClass(){ |
内部类的初始化过程
- 加载外部类: 在使用内部类之前,首先要加载外部类。这包括查找外部类的字节码文件并加载它,但不会初始化内部类。
- 初始化外部类: 如果外部类有静态成员,静态初始化块或静态方法,它们将在加载外部类时进行初始化。这不包括内部类的初始化。
- 创建外部类的实例: 如果您需要使用非静态内部类,您需要首先创建外部类的实例。内部类通常与外部类实例相关联。
- 加载内部类: 内部类的加载通常是在创建外部类的实例后才发生的。加载内部类时,与加载外部类一样,Java会查找内部类的字节码文件并加载它。内部类的字节码文件通常以外部类名$内部类名.class的形式存在。
- 初始化内部类: 内部类的初始化是在加载内部类后,首次使用它之前进行的。这包括静态初始化块、静态成员和构造函数的执行。
需要注意的是,内部类的初始化仅在需要使用内部类时才会发生。这意味着如果您从未实例化外部类或从未使用内部类,那么内部类可能永远不会初始化。
下面是一个示例代码,展示了内部类初始化的过程:
1 | javaCopy codepublic class OuterClass { |
泛型
泛型方法
定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
java 中泛型标记符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
泛型标记符是一种习惯上的标准,便于区别实际调用类型
1 | public class GenericMethodTest |
类型参数是可以有界的
- 上界:
<T extends balabla>
,表示T代表的只能是balabala
及其子类,同时可以声明多个上界,用&
分隔,不建议,有限制规则 - 下界:
<T super balabala>
,表示T代表的只能是balabala
及其超类
1 | public class MaximumTest |
类型通配符?
代表具体的类型参数。例如 List<?> 在逻辑上是 List<String>,List<Integer>
等所有 List<具体类型实参> 的父类。
1 | import java.util.*; |
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。class <T>
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
1 | public class Box<T> { |
文件IO
File file = new File(Path)
,创建新的文件对象,文件对象有很多方法
File.createNewFile
,需要有对应的目录,如果父目录不存在就会报错
File.getParentFile()
,获取父目录文件,如果文件不存在就需要创建父目录
创建父目录有两种
File.mkdir()
创建单级目录
File.mkdirs()
创建多级父目录,是一个递归过程
File.exists()
File.isDirectory()
返回是否是目录File.isFile()
是否是文件File.delete()
,如果是文件就直接删除,如果是空目录就直接删除,否则需要递归删除文件
1 | public static void deleteDirectory(File directory) { |
文件复制
首先是普通的文件复制
给定源文件路径和目标文件路径,拷贝内容
1 | public static void copyFile(String sourceFile,String targetFile) throws IOException { |
然后是目录文件的递归复制
1 | public static void copyDirectory(String sourceDir,String targetDir) throws IOException{ |
字节流Byte Steams
InputStream
和OutputStream
是所有字节流的基类
FileInputStream
从文件中读取字节FileOutputStream
将字节写入文件
1 | FileInputStream fis = new FileInputStream("input.txt"); |
BufferedInputStream
和BufferedOutputStream
提供缓冲,提升效率
1 | InputStream is = new BufferedInputStream(new FileInputStream("input.txt")); |
ByteArrayInputStream
从字节数组中读取数据ByteArrayOtputStream
将数据写入字节数组
1 | byte[] data = { 65, 66, 67 }; |
字符流Character Streams
Reader
和Writer
是所有字符流的基类
FileReader
从文件中读取字符FileWriter
将字符写入文件
1 | FileReader fr = new FileReader("input.txt"); |
BufferedReader
和BufferedWriter
提供了缓冲,提升xiaol
1 | Reader reader = new BufferedReader(new FileReader("input.txt")); |
CharArrayReader
从字符数组里读取数据CharArrayWriter
将数据写入字符数组
1 | char[] data = { 'A', 'B', 'C' }; |
序列化操作
ObjectOutputStream
将对象序列化成字节流
1 | FileOutputStream fileOutputStream = new FileOutputStream(path); |
ObjectInputStream
将字节流重新返回成对象
1 | FileInputStream fileInputStream = new FileInputStream(path); |