java入门
java的版本区别
J2SE:标准版,提供了Java语言最核心的功能,是其他版本的基础
J2EE:企业版,针对企业级应用开发提供了更多高级功能(开发网页使用servlet,连接数据库使用jdbc)
搭建Java开发环境
1.安装Java开发工具包Java Development Kit(JDK)
JDK 6, JDK 7, JDK 8的含义就是J2SE 6,J2SE 7,J2SE 8
下载地址:https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
默认的下载地址是:C:\Program Files\Java\jdk1.8.0_301
我们改成:D:\Program Files\Java\jdk1.8.0_301\
下载完jdk会默认开始下载jre,将其指向和上述jdk一样的目录即可,即D:\Program Files\Java\jre1.8.0_301\
JDK、JRE和JVM之间的关系是什么,就是JDK > JRE > JVM
JDK: java开发包提供开发Java程序必须的工具(包含开发人员需要的工具,比如调试等等)
JRE: java运行环境(普通用户只需要安装JRE,只需要运行java程序)
JVM: Java虚拟机运行程序的核心(java被变成成为字节码.class文件,都是JVM的功劳)
2. 安装Java编码工具Intellij IDEA
到官网下载Community社区免费版即可,关于一些关于IDEA设置的技巧,会在下方不断更新:
语法入门
1. 变量和常量
- Java是严格区分大小写的,类名必须遵守Pascal命名规范,也就是首字母要大写
- 数据类型
- 基本数据类型:数值型(byte、short、int、long、float、double),字符型(char)、布尔型(boolean)
- 引用数据类型:类、接口、数组
- 基本数据类型,比如int整型是4字节,也就是32位二进制表示数值
- Unicode编码成为统一码,是计算机行业为了表示世界各国的文字使用的编码
- 大范围的数据类型转换成小范围的数据类型需要进行强制类型转换
- 常量的定义就是在变量前面添加关键字:final,比如 final int n = 5;
面向对象
类是抽象的概念,是对象的模板
对象是具体的事物,是类的具体实例
// 定义类
public class Dog{}
// 实例化
Dog duoduo = new Dog();
Dog lucky = new Dog();
- 类似于Dog duoduo这种声明对象的方式是在栈当中开辟的一段空间,该空间里的值为null
- 类似于new Dog()这种实例化对象的方式是在堆当中开辟了一段空间,该空间的地址会保存在对应的栈空间中
- 所以基础数据类型的值是直接保存在栈当中,而引用类型的值在栈和堆当中都有保存的值
1.成员变量
成员变量就是隶属于对象的变量
成员变量用于保存对象的静态特征
同类型的不同对象拥有相同的成员变量,但值彼此独立
2. 静态成员
- 静态成员是通过static来修饰的成员变量,它具有这样一个特征:无论该类实例化多少对象,都会公用同一个静态空间。
- 使用static修改的静态成员也叫作类成员,所以它的调用可以直接通过类名调用,而不需要使用类实例化的对象来调用。使用static修饰的静态方法在子类当中不允许被重写。
- static可以用来修饰成员属性和成员方法,但是不能用来修饰类。
- 成员方法中,可以直接访问类中静态成员,但是静态方法中不能直接访问非静态成员,只能访问静态成员(包括静态方法和静态属性)
3.方法
方法是用于完成特定功能的整段代码
public class MethodSample1 {
public String isOddNumber(int num) {
if(num % 2 == 0) {
return "偶数";
} else {
return "奇数";
}
}
public static void main(String[] args) {
Dog erzi = new Dog();
MethodSample1 methodSample1 = new MethodSample1();
// idOddNumber方法是MethodSample1类的方法,所以使用该方法就必须先声明一个methodSample1的实例
erzi.name = methodSample1.isOddNumber(erzi.age);
System.out.println(erzi.name);
}
}
关于构造方法:
- 当没有指定构造方法的时候,系统会自动添加无参的构造方法
- 当有指定构造方法,无论是有参、无参的构造方法,都不会自动添加构造方法
- 一个类当中可以有多个构造方法(函数重载)
关于this:
- 方法当中的this指代的是调用该方法的对象
4. 包-Package
- 包的功能就是把功能相似或者相关的类组织在同一个包中
- 包也采用了树形目录的存储方式(也就是一个包名如果为com.imooc.objectproject.sample,那么实际上就是一个层级目录com/imooc/objectproject/sample)
- 通过包也可以限定类的访问权限
包的命名规范
- 包采用”逆域名法”进行命名,用”.”分割,单词全部小写
- 标准格式:域名后缀.组织机构名.项目名[.模块名].包的职能
比如:com.alibaba.taobao.customer.data的含义就是阿里公司淘宝项目中的客户模块用于存放数据的包
相同包下面的类相互使用不需要引入,但是不同包之间的类如果要相互引用,需要进行导入:
package com.imooc.objectproject.sample2;
import com.imooc.objectproject.sample1.PackageSample01; // import 包.类名
public class PackageSample02 {
public static void main(String[] args) {
PackageSample01 packageSample01 = new PackageSample01();
}
}
如果要导入一个包下面的所有类,那么写法应该是
import com.imooc.objectproject.sample1.*;
5. 访问修饰符
- private 私有 只能在类的内部访问
- public 共有 在任何地方都能访问
- (default) 默认 相同包的其他类可以访问
- protected 继承 只有继承的子类能访问到
范围从小到大 private < default < protected < public
修饰符 本类 同一个包的类 继承类 其他类
private √ ╳ ╳ ╳
default √ √ ╳ ╳
protected √ √ √ ╳
public √ √ √ √
6.代码块
代码块就是通过大括号包起来的一个区域
public class Dog {
{
System.out.println("我是构造代码块");
}
public Dog(){}
static {
System.out.println("我是静态代码块");
}
public void run() {
{
System.out.println("我是普通代码块");
}
}
}
- 普通代码块:顺序执行,先出现,先执行。(每个代码块都是独立的作用域区间,可以重复定义变量,但是不能定义代码块外面的前面已经定义过的变量)
- 构造代码块:创建对象时调用,优先于构造方法执行,每次实例化对象都会调用
- 静态代码块:类加载时调用,优先于构造代码块执行,无论创建多少对象,静态代码块都只执行一次(和单例模式好像很像)
封装
- 封装就是隐藏功能的实现细节
- 利用对象与方法是实现封装的直接途径
- 良好的封装让代码更容易阅读和维护
package com.imooc.objectproject.sample3;
public class MobileNumber {
private String owner; // 所有者
private String areaCode = "86"; // 国家区号
private Long mobileNumber; // 手机号
// 不同的属性其实有不同的读写要求
// 所以应该对成员变量进行读写封装
// getter方法: 用于读取成员变量的内容,格式: public 成员变量类型 get成员变量名()
// setter方法:用户设置成员变量的内容,格式:public void set成员变量()
public String getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getAreaCode() {
return this.areaCode;
}
public Long getMobileNumber() {
return mobileNumber;
}
public void setMobileNumber(Long mobileNumber) {
this.mobileNumber = mobileNumber;
}
}
所以如果一个成员变量是只读不能写的,我们就可以给书写getter方法而不书写setter方法即可。
关于封装的其他知识:
- getter和setter方法可以通过IDE生成,不需要我们手动写,除了自己要书写一些判断程序
继承
package com.imooc.objectproject.sample3;
public class Mark1 {
protected String title;
protected String color;
protected String movie;
public void description() {
System.out.println("型号:" + title);
System.out.println(color);
System.out.println(movie);
}
private void fire() {
System.out.println("利用手臂燃料迸射出火焰");
}
public static void main(String[] args) {
Mark1 mark1 = new Mark1();
mark1.color = "银灰色";
mark1.movie = "钢铁侠1";
mark1.title = "马克1型";
mark1.description();
mark1.fire();
}
}
package com.imooc.objectproject.sample3;
// 子类会继承父类的default/protected/public修饰的成员变量和方法
public class Hulk extends Mark1 {
private void repair() {
System.out.println("弹射备件替换战损组件");
}
public static void main(String[] args) {
Hulk hulk = new Hulk();
hulk.title = "反浩克装甲";
hulk.movie = "复仇者联盟";
hulk.description();
hulk.repair();
}
}
继承的相关知识:
- 继承的关键字为extends,而且Java当中只有单继承,一个子类只能继承一个父类
方法重载的条件:
- 同一个类当中,方法名相同,参数列表不同(顺序,个数,类型)、方法返回值,访问修饰符可以不同
方法重写的条件:
- 在继承关系的子类当中,方法名相同,参数列表相同(参数顺序,个数,类型)。但是方法的返回值和访问修饰符允许在一定的范围当中有变化:
- 返回值和类型如果是自定义的类,那么返回改类的子类也是可以的,但是方法返回值是void或者是基本数据类型时,不允许修改
- 访问修饰符可以在一定范围内改变,就是访问范围需要大于等于父类的访问范围
super关键字:
- super代表的是父类对象的引用
- 通过super可以访问任何父类当中允许子类进行派生的成员
父类的构造方法
- 父类的构造方法是无法被继承的,也无法被重写
子类实例化对象的调用顺序:
- 父类的静态成员
- 父类的静态代码块
- 子类的静态成员
- 子类的静态代码块
- 父类的成员,构造代码块,构造方法
- 子类的成员,构造代码块,构造方法
子类可以选择性的调用父类的构造方法:
- 子类默认调用的是父类的无参构造方法,所以通常建议书写无参构造,在子类对象实例化过程有很重要的作用
- 通过在子类构造函数中调用super(), 选择性的添加参数,就可以选择性的调用父类的有参构造函数
- 子类构造函数当中通过super()调用父类的构造方法的代码必须放在子类构造函数中的第一行
Object类相关:
- 一个类没有使用extends关键字明确标识继承关系,则默认继承Object类(包括数组)
- Object equals() 方法比较两个对象,是判断两个对象引用指向的是同一个对象,即比较 2 个对象的内存地址是否相等。(如果子类重写了 equals() 方法,就需要重写 hashCode() 方法,比如 String 类就重写了 equals() 方法,同时也重写了 hashCode() 方法。)
- Object toString()方法输出的格式是:类型信息+@+地址信息(hashCode的结果),子类可以通过重写toString的方式来改变输出的内容以及表现形式
final关键字
- 有时候我们希望类不被继承,方法不被重写,变量的值不被修改,所以用到了final
- public final class Animal 这种写法表示Animal这个类不允许有子类(比如Java当中的String,System都是被final修饰过的类)。
- public final void eat() 这种写法表示这个eat方法不能在子类当中被重写,final不能修饰构造方法。
- final修饰方法内的局部变量就变成了常量,基本数据类型的常量表示值不变,引用类型的常量表示地址不能变,就是该引用变量不能再重新指向其他地址。
- 而且final修饰的常量不需要在定义的时候就赋值,只需要在第一次使用的时候赋值即可。
- final修饰的成员属性也不需要在定义的时候赋值,但是只能在定义、构造方法和构造代码块当中对成员属性第一次赋值
final关键字和static关键字的区别要注意:
- 类:final修饰意思为类不希望被继承;static不能修饰类
- 成员方法:final修饰成员方法不希望被重写;static修饰表示该方法可以直接通过类名调用
- 成员属性:final修饰成员属性表示整个对象的调用过程中,该成员属性的值无法被修改;static修饰成员属性表示无论类实例化多少个对象,所有对象共用这一个成员属性(共用同一个内存空间)
注解
- 按照运行机制划分。注解的类型分为:
- 源码注解:注解只在源码阶段显示,编译的时候就去掉了,比如@Override
- 编译时注解:注解在编译期保留,JVM在加载class文件会被丢弃,比如说Spring 当中的@NotNull
- 运行时注解:在运行阶段还起作用,甚至会影响运行逻辑的注解,比如说Spring 当中的@Autowird,依赖注入会影响程序
- @Override 是帮助你快速生成和检测重写方法的注解,有没有都不影响程序的运行
单例模式
设计模式是软件开发人员再软件开发过程中面临的一般问题的解决方案,是基于场景的
单例模式的目的就是:使得类的一个对象成为该类系统中的唯一实例,并且自行实例化向整个系统提供
单例模式的优点:
- 内存中只有一个对象,节省内存空间
- 避免频繁的创建销毁对象,提高性能
- 避免对共享资源的多重占用
单例模式的缺点:
- 扩展比较困难
- 如果实例化后的对象长期不利用,系统将默认认为垃圾进行回收,造成对象状态丢失
单例模式的使用场景:
- 创建对象时占用资源过多,但同时又需要用到该类对象
- 对系统内资源要求统一读写,如读写配置信息
- 当多个实例存在可能会引起程序逻辑错误,如号码生成器
所以单例模式的实现要点是:
- 只提供私有的构造方法
- 包含一个该类的静态私有对象
- 提供一个静态的共有方法用于创建和获取静态私有对象
单例模式有两种实现方法:
- 饿汉式:对象创建过程中实例化,空间换时间,速度快 => 提前就创建好唯一实例
- 懒汉式:静态公有方法中实例化,时间换空间,内存少 => 什么时候用什么时候创建
// 饿汉式单例模式
public class SingletonOne {
// 2. 创建静态成员(对象创建的时候就实例化)
private static SingletonOne instance = new SingletonOne();
// 1. 创建私有构造方法
private SingletonOne() {
}
// 3. 创建静态方法
public static SingletonOne getInstance() {
return SingletonOne.instance;
}
// 测试实例
public static void main(String[] args) {
SingletonOne one = SingletonOne.getInstance();
SingletonOne two = SingletonOne.getInstance();
System.out.println(one.equals(two)); // true
System.out.println(one == two); // true
}
}
// 懒汉式单例模式
public class SingletonTwo {
// 2. 创建静态成员(懒汉式方法,创建的时候不去实例化对象)
private static SingletonTwo instance = null;
// 1. 创建私有的构造函数
private SingletonTwo(){
}
// 3. 创建静态的共有方法
public static SingletonTwo getInstance() {
if(SingletonTwo.instance==null) {
SingletonTwo.instance = new SingletonTwo();
}
return SingletonTwo.instance;
}
}
饿汉式是线程安全的,因为在创建对象的时候就创建了实例,而懒汉式存在线程危险,但是在开发过程中,是可以通过同步锁,双重校验锁,静态内部类已经枚举等方法解决。
多态
多态是同一个行为具有多个不同表现形式或者形态的能力
(比如说同一个打印机对于同一个照片可以打印出彩色或者黑白的)
多态的必要条件:
- 满足继承关系
- 父类引用指向子类对象(向上转型,可以调用子类重写父类的方法和父类派生的方法,无法调用子类特有的方法。同时父类中的静态方法无法被子类重写,只能调用到父类原有的静态方法)
子类引用指向父类实例(向下转型,也称强制类型转换,强制类型转换的条件是原本就是子类实例)
1. 类型判断
instanceof: boolean isInstance = obj instanceof Class (判断一个对象是否具有一个类的特征)
- 其他Class.isInstance、Class.isAssignableFrom以及Class.isPrimitive的使用请查阅类型判断
2. 抽象类和抽象方法
使用abstract关键字去描述一类的时候,这个类就无法实例化,只能实例化子类。
- public abstract class Animal {}
使用abstract关键字去描述一个类方法的时候,这个方法就不能具有方法体,而且继承的子类必须去重写该方法,否则子类的定义就会报错。而且抽象方法的前提一定是抽象类。
- public abstract void eat();
3. 接口
- 实现多态的关键是接口,因为Java只支持单继承,所以一个类型当中需要兼容多种类型特征的时候、以及多个不同类型具有相同特征的问题,就需要依靠接口来实现。
- 通过接口可以描述不同的类型具有相似的行为特征。
- 接口是一个抽象的类型,只提供方法的定义
- 实现类是一个接口的具体实现,要实现每一个接口方法的功能
定义接口:
package com.imooc.objectproject.sample5.system;
public interface Language {
// 模拟各国语言的方法定义
public void voice();
}
定义几个实现接口的具体类。
package com.imooc.objectproject.sample5.system;
public class Chinese implements Language{
@Override
public void voice() {
System.out.println("您好,有什么可以帮助您的");
}
}
package com.imooc.objectproject.sample5.system;
public class English implements Language{
@Override
public void voice() {
System.out.println("hello, what can I do for you");
}
}
实现多态:
package com.imooc.objectproject.sample5.system;
public class CustomerService {
public Language contact(int areaCode) {
if(areaCode == 86) {
return new Chinese();
} else if(areaCode == 33) {
return new French();
}else {
return new English();
}
}
public static void main(String[] args) {
Language language = new English(); // 要使用具体的实现类
language.voice(); // hello, what can I do fro you
Language language1 = new Chinese();
language1.voice(); // 你好,有什么可以帮助你的
CustomerService cu = new CustomerService();
Language language2 = cu.contact(45); // 实现多态
language2.voice(); // 根据不同的参数,结果也不一样
}
}
接口有很多注意的点:
- 接口当中的抽象方法可以不写abstract关键字
- 访问修饰符默认为public
- 当类实现接口时,需要去实现接口中的所有抽象方法,否则需要将该类设置为抽象类
- 接口中可以包含常量,默认为public static final来修饰的,接口中的常量访问和static一样,直接通过接口名.常量名称访问即可。但是通过实例访问的常量,如果在子类存在和接口当中一样定义的常量,则结果随着对象的类型变化: ```java public interface IPhoto { int IMPT = 20; // 接口当中定义了常量IMPT }
public class Camera implements IPhoto{ public static final int IMPT = 30; public static void main(String[] args) { IPhoto one = new Camera(); System.out.println(one.IMPT); // 20
Camera two = new Camera();
System.out.println(two.IMPT); // 30
}
}
<a name="gSBQa"></a>
### 4. 默认方法和静态方法
想在有这样的一个情况,在接口当中可能会出现多个抽象方法,但在实现类当中,有时候不会全都都去实现抽象方法,不同的实现类当中可能只会去实现接口中的某个抽象方法而已,这个时候,就出现了默认方法:
```java
public interface IPhoto {
// 普通的方法
public void photo();
// 默认方法
public default void connection() {
System.out.println("我是接口中的默认连接");
}
// 静态方法:也可以带方法体
static void stop() {
System.out.println("我是接口中的静态方法");
}
}
默认方法的作用就是: 能够让实现该接口的子类不需要全部实现所有的接口方法,比如上述代码,实现IPhoto的接口的子类只需要强制实现photo方法,不需要实现connection方法。
静态方法是不能在子类实现接口的时候去强制重写和实现,但是可以继承和调用,只不过接口中的静态方法的使用只能是:接口名.静态方法。
如果在实现接口的子类当中去调用接口中的默认方法和静态方法,有写法规定的:
@Override
public void connection() {
IPhoto.super.connection(); // 调用接口中的默认方法
IPhoto.stop(); // 调用接口中的静态方法
}
5. 多接口重名解决方法
当实现类实现了多个接口,多个接口当中存在名称相同方法,为了解决这个冲动,采用的解决方法就是,在实现类当中重写该方法,这样不论使用类型创造变量对象,调用的方法都是实现类当中独有的方法:
public interface One {
public void eat();
}
public interface Two {
public void eat();
}
public class Instance implements One, Two {
public void eat() {
Syttem.out.println("实例类当中独有的方法")
}
}
public static void main(String[] args) {
One one = new Instance();
one.eat(); // 实例类当中独有的方法
Two two = new Instance();
one.eat(); // 实例类当中独有的方法
}
- 还有一种特殊情况,就是实现类既要继承父类,还有实现多个接口,多个接口当中包含了相同的方法,父类当中也包含同样的方法,那么这种情况下,实现类可以啥都不用干,直接自动继承父类的方法即可。
下面是当多个接口之间包含相同的重名常量该如何解决,采取的方法就是,明确在调用的时候声明调用哪个接口的常量:
interface One {
static int x = 11;
}
interface Two {
final int x = 22;
}
public class Three implements One, Two {
public void test() {
System.out.println(One.x); // 调用One当中的常量
System.out.println(Two.x); // 调用Two当中的常量
}
}
- 还有一种特殊的情况,就是父类当中也包含同名的成员变量或者静态成员变量的时候,此时实现类当中没有任何办法去判断该常量是继承父类的,还是实现哪个接口的,所以解决方法只有:在实现类当中自己定义独有的静态常量或者成员变量。
6. 接口的继承
接口也可以实现继承,和类的继承的区别是:接口可以实现多个继承,类只能实现单继承。
public interface IOne extends ITwo, IThree {}
- 特殊的情况就是多个父接口包含同名的默认方法,那么实现接口只能创建一个自己的默认方法。
7. 内部类
- 在Java当中,可以将一个类定义在另外一个类当中或者一个方法当中,这样的类成为内部类,与之对应,包含内部类的类被成为外部类。
- 内部类隐藏在外部类之类,更好的实现了信息的隐藏
- 内部类有下面四个大类:成员内部类,静态内部类,方法内部类,匿名内部类
成员内部类
内部类的一些要点:
- 内部类在外部使用时,无法直接实例化,需要借由外部类信息才能完成实例化
- 内部类的访问修饰符,可以任意,但是访问范围会受到影响
- 内部类可以直接使用外部类的成员信息,如果内部类出现和外部类同名属性,优先访问内部类(就近原则)
- 内部类当中可以使用外部类.this.成员的方式,访问外部类中同名的信息
// 外部类
public class Person {
int age;
public void eat(){
System.out.println("人会吃东西");
}
// 通常会在外部类当中设置一个获取内部类的方法
public Heart getHeart() {
return new Heart();
}
// 成员内部类
class Heart {
int age = 13;
public String beat() {
eat(); // 直接使用外部类的方法
return Person.this.age + "心脏在跳动"; // 调用外部类的同名成员属性
}
}
}
// 测试用例
public class PersonTest {
public static void main(String[] args) {
Person lili = new Person();
lili.age = 12;
// 获取内部类对象实例,方式1:new 外部类.new 内部类
Person.Heart myHeart = new Person().new Heart();
System.out.println(myHeart.beat());
// 获取内部类对象实例,方式2:外部类对象.new 内部类
myHeart = lili.new Heart();
// 获取内部类对象实例,方式2:外部类对象.获取方法
Person.Heart myHeart2 = lili.getHeart();
}
}
静态内部类
- 静态内部类对象可以不依赖外部类对象,直接创建
- 静态内部类当中只能直接访问外部类的静态方法
- 静态内部类就是通过static关键字来修饰内部类,获取静态内部类创建对象实例也是直接使用
Person.Heart myHeary = new Person.Heart();
方法内部类
- 定义在外部类方法中的内部类,也称局部内部类
- 方法内部成员的使用规则同样适用于方法内部类
- 方法内定义的局部变量只能在方法当中使用
- 方法内不能定义静态成员
- public、private、protected等不能用来修饰
- 方法内部类当中的内部成员和内部方法都是无法使用static来修饰的
- 方法内部类和成员内部类生成的class文件的名称都是特殊的,比如Pearson$1Heart.class这种
public class Person {
int age;
// Heart类是定义在Person类当中的一个方法当中
public Object getHeart() {
class Heart{
public int age = 13;
int temp = 22;
public String beat() {
return "我是方法内部类的方法";
}
}
return new Heart().beat();
}
}
匿名内部类
- 对于有些只使用一次的类而言,它的定义和创建实际可以放在一起去完成,简化对抽象类和接口的操作
- 一般临时用于没有提前写好的类或者临时只使用一次的类 ```java public void getRead(Person person) { person.read(); }
public static void main(String[] args) { PersonTest test = new PersonTest(); // 匿名内部类 test.getRead(new Person(){ @Override public void read() { System.out.pritln(“男生喜欢看科幻类书籍”); } }) }
如上代码,Person是抽象类,无法实例化,所以我们临时创建一个匿名的子类作为参数传入,这就是匿名内部类的一般固定写法。
匿名内部类的一些要点:
- 匿名内部类的生成的class文件的名称和代码所在文件名有关,比如PersonTest$1.class
- 无法使用private, public,protected,abstact, static修饰
- 因为是匿名,所以没有构造函数,但是可以通过构造代码块来实现初始化
- 匿名内部类当中不能出现静态成员
<a name="cda014ef"></a>
## ArrayList类
- ArrayList是Java内置的数据结合,用于存储多个数据
- ArrayList是数组替代品,提供了更多的数据操作方法
- ArrayList几乎是每一个项目必用的类
```java
package com.imooc.objectproject.sample6;
import java.util.ArrayList;
import java.util.List;
public class ArrayListSample {
public static void main(String[] args) {
// List就是个interface接口,而ArrayList就是实现类
List<String> bookList = new ArrayList<String>(); // 泛型
bookList.add("红楼梦");
bookList.add("三国演义");
bookList.add(0,"水浒传"); // add可以添加到固定位置
// System.out.println(bookList);
String bookname = bookList.get(2); // 获取元素
// System.out.println(bookname);
bookList.remove(2); // 删除元素
int length = bookList.size(); // 获取长度
// System.out.println(length);
}
}
引入jar包
- 在项目当中创建一个lib文件夹
- 将jar包复制进入,比如weather-util.jar和weather-util-sources.jar
- 开始引用jar包
- File -> Project Stucture -> Libraries -> 点击+号 -> 选择java
- 在目录当中选择当前工程/lib -> 选择weather-util.jar -> ok
- 会提示你: Library weather-util will be added to the selected modules
- 接着你就会发现,工程里lib目录下的jar可以展开了,就可以看源码
- 具体的使用要看包的说明
字符串格式化
String template = "%s-%s-%s";
String row = String.format(template, new String[]{a,b,c})
异常
1. 异常分类
Java当中所有异常类都继承于Throwable,而Throwable主要分为两个类:Error和Exception
- Error叫做错误,一般是很少接触,它指的是虚拟机错误(VirtualMachineError)、线程死锁(ThreadDeath)等等,最好不要有这种错误出现,因为一旦出现,程序就崩溃了,因为Error是硬伤,是应用应用程序的控制和处理能力之外的。
- Exception叫做异常,通常指的是编码,环境,用户操作输入出现问题。它有很多的子类,比如RuntimeException(运行时异常或者非检查异常)、其他异常(也称检查异常)
非检查异常RuntimeException又分为很多种类,比如
- 空指针异常:PointerException
- 数组下标越界异常: ArrayIndexOutOfBoundsException
- 类型装换异常:ClassCastException
- 算术异常:ArithmeticException
等等,还有很多,运行时异常会由虚拟机自动捕获和抛出,说明程序有问题
其他异常也有很多类型,比如;
- 文件异常:IOException
- SQL异常:SQLException
这种需要你手动去添加捕获的语句
- 运行时异常也叫作非检查异常,是不强求使用try和catch去检查的,其他的必须要处理异常。所以并不建议在书写程序的时候抛出运行时异常(也称非检查异常),尽量去抛出必须要检查的异常,比如Exception。
- 有的时候我们希望遇到某些异常的时候无条件去终止程序,这个时候我们可以使用System.exit(1)这样的语句。
2. 处理异常
try {
// 一些会抛出异常的方法
} catch(Exception e) {
// 处理改异常的代码块
}
比如我们现在有一个捕获输入错误的方法:
try {
System.out.print("请输入你的年龄");
Scanner input = new Scanner(System.in);
int age = input.nextInt();
System.out.println("十年后你" + (age + 10) + "岁");
} catch (InputMismatchException e) {
System.out.println("你应该输入整数");
}
对于同一段程序来讲会出现多个不同类型的错误,我们需要使用多重捕获,比如下面这段代码
Scanner input = new Scanner(System.in);
try {
System.out.print("请输入第一个数");
int one = input.nextInt();
System.out.print("请输入第二个数");
int two = input.nextInt();
System.out.println("两个数相除的结果为:" + ont/two);
} catch (InputMismatchException e){ // 捕获子类
System.out.print("你应该输入整数");
} catch (ArithmeticException e) { // 捕获子类
System.out.print("除数不能为0")
} catch (Exception e) { // 捕获父类
e.printStackTrace(); // 打印出异常信息和错误位置
System.out.println("我是不知名的异常")
} finally {
// 最终执行的代码,比如关闭文件,断开数据库等等
}
特别要注意的点就是:
- 多个catch的写法顺序一定是子类在前,父类在后。
- 不要轻易在finally当中书写return语句,因为finally比retrun更优先,即便在try或者catch当中书写了return,程序也不会中断,会继续走finally的逻辑。
3. 抛出异常
抛出异常的两个关键字:throw和throws
throw 是将产生的异常抛出,是要写在方法体当中的
throws 是将异常继续向更外层抛出,是写在方法体外面的
来看下面这个例子:
public void divide(int one, int two) throws Exception {
if(two == 0) {
throw new Exception("两数相除,除数不能为0");
} else {
System.out.println("两数相除,结果为:", one/two)
}
}
divide的调用方如果可以处理这样的异常就直接使用try/catch
public void compute() {
...
try {
divide(5,0)
} catch (Exception e) {
System.out.println(e.getMessage()) // e.getMessage()只会打印出异常的名称
}
}
如果divide的调用方是不能处理异常的层级,就应该将divide抛出的错误继续抛出:
public void compute() throws Exception {
divide(5, 0);
}
值得注意的就是:throws后面可以跟随多个异常类型,之间使用逗号隔开,表示该方法可能会产生多种错误异常,调用该方法的地方需要对所有可能产生的错误类型一一进行catch捕获。
4. 自定义异常
class 自定义异常类 extends 异常类型 {}
public class DrunkException extends Exception {
// 构造无参构造器
public DrunkException() {}
// 构造有参构造器
public DrunkException(String message) {
super(message)
}
}
5. 异常链
有时候可以把捕获的异常包装成为新的异常,然后在新的异常当中添加对原始异常类的引用,然后再把这个异常抛出,就像是链式反应一样。
比如我们下面这个例子就是test1抛出喝大了异常,test2调用test1,捕获喝大来的异常,并且包装成运行时异常,在main方法当中捕获test2当中的运行时异常
public void test1() throws DrunkException{
throw new DrunkException("喝酒别开车");
}
public void test2 throws RuntimeException{
try {
test1()
} catch(DrunkException e) {
RuntimeException newExc = new RuntimeException("司机一滴酒,亲人两行泪");
newExc.initCause(e); // 异常的包装器,newExc会包含原始e当中的错误信息
// 当然也可以直接书写:RuntimeException newExc = new RuntimeException(e);
throw newExc;
}
}
public static void main(String[] args) {
ChainTest ct = new ChainTest();
try{
ct.test2();
}catch(Exception e) {
e.printStackTrace() // 显示RuntimeException异常,其中包含DrunkException异常
}
}
字符串
1. 创建字符串
创建字符串的三个方法:
String s1 = "taopopy";
String s2 = new String();
String s3 = new String("taopoppy");
2. 字符串的不变性
- String对象创建后则不能被修改,是不可变的,所谓的修改其实是创建了新的对象,所指向的内存空间不同
- 所以对比两个字符串的值是否相同,使用.equals方法,使用== 对比的是连个对象在内存中首地址
- 如果需要一个可以改变的字符串,我们可以使用StringBuffer或者StringBuilder
3. 字符串常用的方法
4. 可变字符串
- 因为通过String去定义一个字符串,当频繁操作字符串时,就会额外产生很多临时变量,因为字符串的不可变性,所以是覆盖关系。
- 使用StringBuilder或StringBuffer就可以避免这个问题。至于 StringBuilder和StringBuffer ,它们基本相似,不同之处,StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此一般情况下,如果需要创建一个内容可变的字符串对象,应优先考虑使用StringBuilder类。
StringBuilder str1 = new StringBuilder("taopoppy")
str1.append("zhenchuan"); // 字符串后面追加字符串
str1.append(520); // 字符串后面追加整数
str.insert(11,"!");
5. 字符串比较
这里又涉及到了equals方法和==操作符的比较,我们追本溯源,在Object当中的equals方法当中的比较手段就是==返回的结果,所以equals和==的本质都是比较两者在内存当中的地址。只不过继承Object的很多子类对equals方法进行了重写,所以有些类的equals方法用来比较值的,而不是比较地址。String就是很好的例子,String重写了equals方法,所以使用xxx.equals(yyy)可以用来比较两个字符串的字面量值。
但是你会发现,两个不同的字符串字面量,通过==比较两个字符串的地址也是相同的,这个就涉及到了常量池,我们先看一下下面代码:
String str1 = "taopoppy";
String str2 = "taopoppy";
String str3 = new String("taopoppy");
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
str1使用equals方法比较两个字符串的值,所以str1和str2与str3的值都一样,结果为true,但是为什么str1==str2的结果为true,而str1==str3的结果为false呢?我们来看这个图:
首先,创建Str1之后,字符串”taopopy”就保存在了常量池当中,然后Str2创建的时候,发现常量池当中有了”taopoppy”,所以Str2也指向了这个地址,所以Str1和Str2的地址在常量池当中是一样的,但是通过new关键字去创建字符串对象后是在堆中产生了新的地址所以Str1和Str3是不一样的地址。所以我们就明白两个要点:
- 字符串和数字类型都会遵循各自的规则将值保存在常量池当中
- 字符串的比较在实际应用当中最好使用equal方法比较字面量值,而不是地址
包装类
1. 包装类概述
int、float、double、boolean、char 等。基本数据类型是不具备对象的特性的,比如基本类型不能调用方法、功能简单。。。,为了让基本数据类型也具备对象的特性, Java 为每个基本数据类型都提供了一个包装类,这样我们就可以像操作对象那样来操作基本数据类型。
基本类型和包装类之间的关系:
包装类主要提供了两大类方法:
- 将本类型和其他基本类型进行转换的方法
- 将字符串和本类型及包装类互相转换的方法
我们举个最简单的例子:
// 定义int类型变量,值为86
int score1 = 86;
// 创建Integer包装类对象,表示变量score1的值
Integer score2=new Integer(score1);
// 将Integer包装类转换为double类型
double score3=score2.doubleValue();
// 将Integer包装类转换为float类型
float score4=score2.floatValue();
// 将Integer包装类转换为int类型
int score5 =score2.intValue();
System.out.println("Integer包装类:" + score2);
System.out.println("double类型:" + score3);
System.out.println("float类型:" + score4);
System.out.println("int类型:" + score5);
2. 包装类和基本类的转换
在JDK1.5引入自动装箱和拆箱的机制后,包装类和基本类型之间的转换就更加轻松便利:
装箱:把基本类型转换成包装类,使其具有对象的性质,又可分为手动装箱和自动装箱
int i = 20; // 定义基本类型
Integer x = new Integer(i); // 手动装箱
Integer y = i; // 自动装箱(直接使用包装类型即可)
拆箱:把包装类对象转换成基本类型的值,又可分为手动拆箱和自动拆箱
Integer j = new Integer(8); // 定义一个包装类
int m = j.intValue(); // 手动拆箱
int n = j; // 自动拆箱为int类型
3. 基本类和字符串转换
基本类型转换为字符串有三种方法:
- 使用包装类的toString()方法
- 使用String类的valueOf()方法
- 用一个空字符串加上基本类型,得到的就是基本类型数据对应的字符串
int c = 10;
String str1 = Integer.toString(c); // 方法一
String str2 = String.valueOf(c); // 方法二
String str3 = c + ""; // 方法三
将字符串转换成基本类型有两种方法:
- 调用包装类的 parseXxx 静态方法
- 调用包装类的 valueOf() 方法转换为基本类型的包装类,会自动拆箱
String str = "8";
int d = Integer.parseInt(str); // 方法1
int e = Integer.valueOf(str); // 方法2
4. 包装类的比较-常量池
我们来看一段代码:
Integer one = new Integer(100);
Integer two = new Integer(200);
System.out.println(one==two); // false, ==在对象直接比较的是对象的内存地址
System.out.println(one.equals(two)); // false, Object.equals比较的是对象的内存地址
Integer three = 100; // 自动装箱
System.out.println(three == 100); // true,对象和基础类型比较会自动拆箱,比较数值相同
System.out.println(three.equals(100)); // true,包装类的equals比较的是数值的大小
Integer four = 100;
// Integer four = Integer.valueOf(100);
System.out.println(three == four);
// true,因为包装类在拆装箱的时候,会提供一个缓存区,在-128<=参数<=127,那么直接从缓存中查找是否有这样的对象
// 如果有直接产生,如果没有,实例化Integer
// 所以在此之前three在缓存区当中已经有了100这个对象,所以four在创建的时候直接从缓存区当中直接产生,两者在缓存区地址相同
Integer five = 200;
System.out.println(five == 200); // true, five会自动拆箱,比较数值大小
Integer six = 200;
System.out.println(six == five);
// false, 虽然数值相同,但是five不在-128-127内,不在缓存区,所以six也不在缓存区
// five和six都是在内存当中创建的新内存,所以内存地址不同
System.out.println(six.equals(five)); // true,包装类的equals比较的数值相同
可以看到,存在缓存区或者常量池这样一个概念,但是特别要注意:所有的基本类型当中只有double和float两个类型没有常量池这样的概念。
5. Date时间类型
将时间类型转换成字符串类型:
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String today = sdf.format(d):
将字符串转换成时间类型:
String day = "2014年02月3号 10:33:49";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date date = df.parse(day);
- 调用SimpleDateFormat对象的parse()方法时可能会出现转换异常,即ParseException ,因此需要进行异常处理
- 使用Date类时需要导入java.util 包,使用SimpleDateFormat时需要导入java.text包
6. Calendar时间类型
java.util.Calendar类是一个抽象类,可以通过调用getInstance()静态方法获取一个Calendar对象,此对象已由当前日期时间初始化,即默认代表当前时间,如Calendar c = Calendar.getInstance();
// 创建Calendar对象
Calendar c = Calendar.getInstance();// 创建Canlendar对象
int year = c.get(Calendar.YEAR); // 获得年份
int month = c.get(Calendar.MONTH) + 1 ; // 获取月份
Long time = c.getTimeInMillis(); // 获取毫秒数:1628492291917
// 将Calendar对象转换为Date对象
Date date = c.getTime();
// 创建SimpleDateFormat对象,指定目标格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 将日期转换为指定格式的字符串
String now = sdf.format(date);
System.out.println("当前时间:" + now);
System.out.println(time);
Math类操作
Math类位于java.lang包中,包含用于执行基本数学运算的方法,Math类的所有方法都是静态方法,所以使用该类中的方法时,可以直接使用类名.方法名,如:Math.round();
Math的方法很多,需要的时候直接上网查即可。
集合框架
1. 集合概述
集合的概念
- 现实生活中的集合:很多的事物凑在一起
- 数学中的集合:具有共同属性的事物的总体
- Java中的集合类:是一种工具类,就像是容器,存储任意数量的具有共同属性的对象
集合的作用:
- 在类的内部,对数据进行组织
- 简单而快速的搜索大数量的条目
- 有的集合接口,提供了一系列排列有序的元素,并且可以在序列中间快速的插入或者删除有关元素
- 有的集合接口,提供了映射关系,可以通过关键字(key)去快速查找到对应的唯一对象,而这个关键字可以是任意类型
为什么使用集合而不是数组
- Java当中的数组是固定长度的,集合的长度是动态变化的
- 数组只能通过下标访问元素,类型固定,而有的集合可以通过任意类型查找所映射的具体对象
Java当中集合主要有两个大家族:Collection和Map,这是两个最顶层的接口
- Collection
- List(序列):元素排列有序而且可以重复
- ArrayList: 数组序列
- List(序列):元素排列有序而且可以重复
- Queue(队列):元素排列有序而且可以重复
- LinkedList:链表
- Set(集):元素无序,且不可以重复
- HashSet: 哈希集
Collection接口
- 是List、Set和Queue接口的父接口
- 定义了可用于操作List、Set和Queue的方法-增删改查
List接口和ArrayList
- List可以精确的控制每个元素的插入位置,或者删除某个元素
- ArrayList-数组序列,是List的一个重要实现类
- ArrayList底层是由数组实现的,所以在ArrayList列表尾部插入或者删除数据非常有效,是不适合中间插入删除元素,涉及到大量数组的删除和其他操作。其次ArrayList更适合查找和更新元素。
public class ListTest {
public List coursesToSelect;
public ListTest() {
this.coursesToSelect = new ArrayList();
}
/**
* y用于往coursesToSelect中添加备选课程
*/
public void testAdd(){
Course cr1 = new Course("1","数据结构");
Course cr2 = new Course("2", "C语言");
Course[] course = {new Course("3", "离散数据"), new Course("4", "量子力学")};
Course[] course1 = {new Course("6", "离散物理"), new Course("7", "量子纠缠")};
// ----------------------------------添加----------------------------------------
coursesToSelect.add(cr1);
coursesToSelect.add(0, cr2);
coursesToSelect.addAll(Arrays.asList(course));
coursesToSelect.addAll(0,Arrays.asList(course1));
// ----------------------------------查询----------------------------------------
// 1. (for循环+get方法)
for (int i = 0; i< coursesToSelect.size(); i++) {
Course item = (Course) coursesToSelect.get(i);
System.out.println(item.getId() + ":" + item.getName());
}
// 2. 迭代器(迭代器没有存储功能,只能依赖于某个集合存在,返回元素是无序的)
Iterator it = coursesToSelect.iterator(); // 放回ArrayList的一个迭代器
while(it.hasNext()) {
Course cr = (Course) it.next();
System.out.println(cr.getId() + ":" + cr.getName());
}
// 3. forEach方法(迭代器的简便写法)
for(Object obj: coursesToSelect) { // 这里依然是一个元素存储到集合后,类型被忽略,所以取的时候是Object类型
Course cr = (Course) obj;
System.out.println(cr.getId() + ":" + cr.getName());
}
// ----------------------------------修改----------------------------------------
coursesToSelect.set(4, new Course("7", "毛概"));
// ----------------------------------删除----------------------------------------
// 1. 删除指定对象
Course cr = (Course) coursesToSelect.get(4);
coursesToSelect.remove(cr);
// 2. 删除指定下标
coursesToSelect.remove(1);
// 3. 组团删除
Course[] couses2 = {(Course) coursesToSelect.get(2), (Course) coursesToSelect.get(3)};
coursesToSelect.removeAll(Arrays.asList(couses2));
Course temp = (Course) coursesToSelect.get(0); // 因为对象存入集合都会变成Object类型,取出的时候需要强制转换
// ----------------------------------是否包含元素----------------------------------------
coursesToSelect.contains(course); // true或者false
// contains方法的实现是数组中每个元素都调用equals方法,判断两个对象是否相同
// 所以如果你有这样的需求,两个元素的某个特定的属性相同就返回true,就需要自己重写equals方法
// ----------------------------------判断索引位置----------------------------------------
coursesToSelect.indexOf(course);
// indexOf的原理也是循环调用每个元素的equals方法,为true的时候将此索引返回
}
}
// Course.java
public class Course {
@Override
public boolean equals(Object obj) {
if(this == obj) { // 引用类型之间使用== 表示比较的是内存地址
return true;
}
if(obj == null) {
return false;
}
if(!(obj instanceof Course)) {
return false;
}
Course course = (Course) obj;
if(this.name == null) {
if(course.name == null) {
return true;
} else {
return false;
}
} else {
if(this.name.equals(course.name)) {
return true;
} else {
return false;
}
}
}
}
3. Set
- Set和List最大的区别就是Set元素是无序且唯一的。
- 所以Set没有List当中的set方法去修改元素,没有List的索引。
- Set的一些使用方法几乎和List差不多,但是还有一些区别:
- Set当中的循环只能使用foreach和迭代器,以为元素是无序的,所以Set没有get取数据一说
- Set当中的元素是唯一的,对同一个对象保存多次,Set也会有一个对象的引用
- Set当中也包含contains方法,这个方法的原理是先调用的hashCode,再调用equals方法,前者是用来将对象转换成哈希码判断相等,后者是用来判断内存地址的,所以contains方法要重写需要重写hashCode和equlas两个方法。(IDEA当中直接右键,在generate选项当中有生成equals和hasCode方法)
4. Map
- Map提供了一种映射关系,其中的元素是以键值对的形式存在的
- Map中的键值对以Entry类型的对象实例形式存在,可以理解为Entry类型的对象实例包含key和value
- Map当中的key不可以重复,value可以重复
- Map支持泛型,形式如 Map
Map都有哪些常用的方法呢?
- 添加元素:put(K key, V value)
- 删除元素:remove(Object key)
- 返回元素集合:
- keySet(): 返回所有键的Set视图
- values(): 返回所有值的Collection视图
- entrySet(): 返回映射关系的Set视图
HashMap类
- HashMap是Map的一个重要实现类,也是最常用的,基于哈希表实现
- HashMap中的Entry对象是无序排列的
- Key值和Value值都可以为null,但是一个HashMap只能有一个key值为null的映射
public class MapTest{
public static void main(String[] args) {
// 创建 HashMap 对象 Sites
HashMap<Integer, String> Sites = new HashMap<Integer, String>();
// -------------------------添加键值对-----------------------
Sites.put(1, "Google");
Sites.put(2, "Runoob");
Sites.put(3, "Taobao");
Sites.put(4, "Ali");
// -------------------------访问值--------------------------
Set<Integer> ketSey = Sites.keySet(); // keySet方法返回所有键的集合
Iterator<Integer> iterator = ketSey.iterator();
while (iterator.hasNext()) {
String val = Sites.get(iterator.next()); // get方法取得key对应的value
}
// -------------------------访问映射-------------------------
// 返回的是一个Set集合,集合的元素是一个Entry类型
Set<Map.Entry<Integer, String>> entrySet = Sites.entrySet();
for(Map.Entry<Integer, String> entry:entrySet) {
System.out.println(entry.getKey());
}
// --------------------------删除元素-------------------------
if(Sites.containsKey(2)) {
Sites.remove(2);
}
// --------------------------修改元素-------------------------
// 修改元素的本质就是覆盖
Sites.put(1, "Chinese");
// --------------------------是否包含值-------------------------
Sites.containsKey(1); // 是否包含1这个key值
Sites.containsValue("Chinese"); // 是否包含Chinese这个value值
// Map的contains方法背后的原理也是调用每个值的equals方法
}
}
5. 集合排序
- 使用Colleactions类的sort()方法
sort(List
list) - 根据元素的自然顺序对指定列表按升序进行排序 list中的整型数据进行排序:
List<Integer> list = new ArrayList<Integer>();
list.add(5);list.add(9);list.add(3);list.add(1);
// 排序
Collections.sort(list); // 1 3 5 9
list中的字符串进行排序
List<String> list = new ArrayList<String>();
list.add("orange");list.add("blue");list.add("yellow");list.add("red");
// 排序
Collections.sort(list); //按照ASC码排序
Comparator自定义类排序
- Comparator接口(强行对某个对象进行整体排序的比较函数,可以将Comparator传递给sort方法,如Collections.sort或Arrays.sort)
- int compare(T o1, T o2)比较用来排序的两个参数,o1>o2返回正整数,相等返回0,否则返回负整数
- Comparator接口(强行对某个对象进行整体排序的比较函数,可以将Comparator传递给sort方法,如Collections.sort或Arrays.sort)
也就是说我们实现Comparator这个接口的时候只需要去实现重写compare这个方法,现在我们比较猫这个类
public class NameComparator implements Comparator<Cat> {
@Override
public int compare(Cat o1, Cat o2) {
// 按照名字升序排序
String name1 = o1.getName();
String name2 = o2.getName();
int n = name1.compareTo(name2);
return n;
}
public static void main(String[] args) {
Cat huahua = new Cat("huahua", 5, "英国短腿猫");
Cat fanfan = new Cat("fanfan", 2, "中华田园猫");
Cat maomao = new Cat("maomao", 2, "中华田园猫");
List<Cat> catList = new ArrayList<Cat>();
catlist.add(huahua);
catlist.add(fanfan);
catlist.add(maomao);
// 测试自定义类进行比较
Collections.sort(catList, new NameComparator());
}
}
- Comparable接口
- 此接口强行对实现它的每个类的对象进行整体
- 这种排序被成为类的自然排序,类的compareTo方法被成为它的自然比较方法
- 和Comparator不同,Comparable是比较类通过实现Comparable当中的compareTo方法来实现的
下面来实现一个商品的比较:
public class Goods implements Comparable<Goods>{
@Override
public int compareTo(Goods o) {
int price1 = this.price;
int price2 = o.price;
return price1-price2;
}
}
泛型管理
1. 泛型概述
- 集合中的元素,可以是任意类型的对象(对象的引用)
- 如果把某个对象放入集合,则会忽略他的类型,而把他当做Object处理
- 泛型则是规定了某个集合只可以存放特定类型的对象,并且在编译期间进行类型检查,可以直接按指定类型获取集合元素
public class TestGeneric {
public List<Course> courses;
// 构造函数
public TestGeneric() {
this.courses = new ArrayList<Course>();
}
// 测试函数
public void testAdd() {
// ---------------------------------- 泛型添加 ------------------------------------
Course cr1 = new Course("1", "大学语文");
courses.add(cr1);
// 泛型集合当中,不能添加泛型规定的类型(包含子类型)以外的对象,否则会报错
Course cr2 = new Course("1", "Java基础");
courses.add(cr1);
// ---------------------------------- 泛型循环 ------------------------------------
for(Course cr:courses) {
System.out.println(cr);
// 正是因为courses是规定了类型的集合,所以取出来就直接是Course类型,而不是Object类型
}
// ---------------------------------- 泛型存储 ------------------------------------
// 泛型集合不仅能存储泛型类型,还能存储泛型的子类型
ChildCourse ccr = new ChildCourse();
// ---------------------------------- 泛型类型的规定 -------------------------------
// 泛型不能使用基本类型,只能使用其包装类
List<int> list = new ArrayList<int>(); // 错误
List<Integer> list1 = new ArrayList<Integer>(); // 正确
}
}
2. 泛型作为方法参数
泛型作为方法参数有两种选择,一种就是只能传递一种泛型类型:
public void sellGoods(List<Goods> goods) {}
另一种就是传递规定类型和子类型都可以的:如下代码所示,Goods类包括Goods的子类都可以作为泛型类型传递进来。
public void sellGoods(List<? extends Goods> goods) {}
3. 自定义泛型类和泛型方法
定义一个泛型类是比较容易的,多个泛型类型也是如此,只是在
// 单个泛型类型
public class NumGeneric<T> {
private T num;
public T getNum() {
return num;
}
public void setNum(T num) {
this.num = num;
}
// 测试
public static void main(String[] args) {
NumGeneric<Integer> intNum = new NumGeneric<>();
intNum.set(10);
System.out.println(intNum.getNum()); // 10
}
}
定义一个自定义泛型方法的写法比较固定如下:
public <T> void printValue(T t){
System.out.println(t);
}
public <T extends Number> printNumValue(T t) {
System.out.println(t);
}
public static void main(String[] args) {
printValue("Hello"); // 正确
printNumValue("Hello"); // 错误,参数只能是Numner或者Number的子类,Integer,Float等
}
可以看到,泛型方法就是约束了方法参数和方法体当中的一些变量类型要统一而已。
多线程
- 什么是进程:进程是可执行程序并存放在计算机存储器的一个序列,它是一个都动态执行的过程。
线程是比进程还要小的运行单位,一个进程多个线程,这个就是线程。
1. 线程的创建-Thread类
创建一个Thread类,或者一个Thread子类的对象
- 创建一个实现Runnable接口的类的对象
首先来看线程Thread类构造方法:
线程有下面一下常用的方法:
- run方法:线程中最重要的方法,不同线程之间的区别就在于这个方法当中代码不同
我们来看一个简单的例子:
package com.imooc.thread;
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println(getName()+"正在运行第"+ i+ "次");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread("线程2");
mt1.start();
mt2.start();
}
}
上述代码的的运行结果在每次运行都不一样,因为无法确保cpu将怎么分配处理时间,我们可以看下面干这个图,你就知道cpu在随机分配处理线程的时间:
cpu在不同的线程之间跳来跳去,所以线程的执行时间是随机的。
2. 线程的创建-Runnable接口
我们下面再来看看Runnable接口:
- 只有一个方法run()
- Runnable是Java中用于实现线程的接口
- 任何实现线程功能的类都必须实现改接口(public class Thread implements Runnable)
那为什么之前线程的创建已经有Thrread类了,为何还有Runnable接口?就是因为如果当你的类除了要继承Thread类还有继承别的类的功能就没法实现了,因为Java不支持多继承,所以就要通过实现多接口的方式
下面我们来实现一下Runnable,可以看到我们实例化了一个实现Runnable的对象后,依然要通过Thread的方式去实现创建线程,因为Thread有不同的构造函数。
class PrintRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}
public class ThreadTest {
public static void main(String[] args) {
PrintRunnable pr = new PrintRunnable();
Thread t1 = new Thread(pr);
t1.start();
}
}
另外我们举一个最简单的双线程共享资源的例子,两个线程Thread子类t1和t2同时共享了一个PrintRunnable对象,当中的成员变量i也被同时共享,所以一共打印输出了10次,只不10次当中由哪个线程打印输出是看cpu随机分配的。
class PrintRunnable implements Runnable{
int i = 0;
@Override
public void run() {
while (i<10) {
System.out.println(Thread.currentThread().getName() + "正在运行" + (i++));
}
}
}
public class ThreadTest {
public static void main(String[] args) {
PrintRunnable pr = new PrintRunnable();
Thread t1 = new Thread(pr);
t1.start();
Thread t2 = new Thread(pr);
t2.start();
}
}
3. 线程的状态和生命周期
- 新建(new): 创建一个Thread对象或者Thread子类的对象的时候,进入新建状态
- 可运行(Runnable): 使用创建的线程对象调用start方法的时候就进入了可运行状态
- 正在运行(Running): 一个处于可运行状态的线程一旦获取了cpu的使用权就可以进入正在运行状态
- 阻塞(Blocked): 线程遇到干扰不再执行就进入了阻塞状态
- 终止(Dead): 线程终止
线程的状态之间可以进行切换,通过调用Thread的不同的方法进行切换
4. 线程调度
sleep方法
- sleep方法是Thread类的静态方法: public static void sleep(long millis)
- 在指定的毫秒数内让正在执行线程休眠(暂停执行)
- sleep方法是需要通过trycatch去包裹的,因为会产生InterrupedException的错误
- sleep会让程序进入进入阻塞状态,然后阻塞过期后就自动进入了Runnable状态,所以实际上下面的代码使用sleep模拟了一个定时的方法,定时实际上是不准的,因为真正在复杂情况下不知道什么时候cpu会分配给Runnable使用权。
```java
class PrintRunnable implements Runnable{
@Override
public void run() {
} }for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + "正在运行" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public class ThreadTest { public static void main(String[] args) { PrintRunnable pr = new PrintRunnable(); Thread t1 = new Thread(pr); t1.start(); } }
<a name="cwJiQ"></a>
#### join方法
- join方法是Thread类当中无法重写的一个方法:
- public final void join() => 等待调用方法的线程结束后才能执行,优先抢占资源
- public final void join(long millis) => 等待该线程终止的最长时间为millis毫秒,带限制性的抢占资源,比如10millis,意思就是该线程抢占10millis,时间就将cpu使用权让出去。
```java
class MyThread extends Thread {
public void run() {
System.out.println(getName() + "正在运行");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
try {
t1.join(); // 等待t1线程优先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程运行结束");
}
}
线程优先级
- Java为类提供了10个优先级
- 优先级可以用1-10表示,超出范围会抛出异常,数字越大,优先级越高
- 主线程默认的优先级为5
- 除了数字,还可以使用优先级表示
- Thread.MAX_PRIORITY: 线程的最高优先级10
- Thread.MIN_PRIORITY: 线程的最低优先级1
- Thread.NORM_PRIORITY: 线程的默认优先级5
- 优先级相关方法:
- public int getPriority() =>获取线程优先级的方法
- public void setPriority(int newPriority) => 设置线程优先级的方法
- 获取主线程的方法:
- Thread.currentThread() => 获取到当前线程,写在main方法中就是获取主线程
5. 同步死锁
线程同步
- 多线程运行问题
- 各个线程是通过竞争CPU时间而获取运行机会的
- 各线程什么时候得到CPU时间,占用多久,是不可预测的
- 一个正在运行着的线程在什么地方被暂停是不确定的
比如我们来看这个银行存取款的问题:
package com.imooc.thread;
// 存款类
public class SaveAccount implements Runnable{
Bank bank;
public SaveAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.saveAccount();
}
}
package com.imooc.thread;
public class DrawAccount implements Runnable{
Bank bank;
public DrawAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
package com.imooc.thread;
public class Bank {
private String account; // 账号
private int balance; // 账户余额
public Bank(String account, int balance) {
this.account = account;
this.balance = balance;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Bank[账号: " + account + ", 余额:" + balance + "]";
}
// 存款
public void saveAccount() {
// 可以在不同的位置处添加sleep方法
// 获取当前的账号余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改余额,存100元
balance+= 100;
// 修改账户余额
setBalance(balance);
System.out.println("存款后的账户余额为:"+ balance);
}
// 取款
public void drawAccount() {
// 在不同的位置处添加sleep方法
// 获得当前的账户余额
int balance = getBalance();
// 修改余额,取200
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改账户余额
setBalance(balance);
System.out.println("取款之后的账户余额: " + balance);
}
public static void main(String[] args) {
// 创建账户,给定余额1000
Bank bank = new Bank("1001", 1000);
// 创建线程对象
SaveAccount sa = new SaveAccount(bank);
DrawAccount da = new DrawAccount(bank);
Thread save = new Thread(sa);
Thread draw = new Thread(da);
save.start();
draw.start();
try {
save.join();
draw.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank);
}
}
由于cpu分配给线程使用的不稳定性,上述代码经常会出现错误,比如说对于同一个对象bank对象,SaveAccount方法执行到43行完毕后,突然去执行DrawAccount方法,最终的结果就是DrawAccount方法执行完毕bank对象中的余额是800,然后SaveAccount中的balance变量是1100,SaveAccount操作bank对象就会将800的结果覆盖掉。所以取200存100最终的结果反而是1100。
针对上面的问题我们需要将Bank对象进行锁定,也就是同一个时刻只能有一个线程对共享资源进行操作,即线程互斥:
- 使用关键字synchronized实现,那是怎么使用的呢?
- 用在成员方法: public synchronized void saveAccount() {}
- 用在静态方法: public static synchronized void saveAccount(){}
- 用在语句块: synchronized (obj){….}
所以我们上面的在Bank对象当中的取款和存款方法可以有两种改法:
public synchronized void saveAccount() { // 给成员方法使用synchronized
int balance = getBalance();
balance+= 100;
setBalance(balance);
System.out.println("存款后的账户余额为:"+ balance);
}
// 取款
public void drawAccount() {
synchronized(this) { // 使用语句块的方式将其代码包裹
int balance = getBalance();
balance = balance - 200;
setBalance(balance);
System.out.println("取款之后的账户余额: " + balance);
}
}
线程通信
- wait()方法:中断方法的执行,使线程等待
- notify()方法: 唤醒处于等待的某一个线程,使其结束等待
- notifyAll()方法: 唤醒所有处于等待的线程,结束它们等待
- 死锁:线程都处于阻塞状态
我们现在要设置一个场景,就是消费者和生产者同时在一个容器当中进行存取的场景,要求生产一个消费一个,不同存在同时生产两个和同时消费两个的情况发生:
package com.imooc.queue;
public class Consumer implements Runnable{
Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
queue.getN();
}
}
}
package com.imooc.queue;
public class Producer implements Runnable{
Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while (true) {
queue.setN(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.imooc.queue;
public class Queue {
private int n; // 当前容器里的值
boolean flag = false; // 当前容器是否有值
public synchronized int getN() {
if(!flag) {
try {
wait(); // 当容器当中没有值的时候需要消费者线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费:" + n);
flag = false; // 消费完毕,容器当中没有数据
notifyAll(); // 唤醒所有线程(唤醒消费线程,消费线程依旧等待,唤醒生产线程继续生产)
return n;
}
public synchronized void setN(int n) {
if(flag) {
try {
wait(); // 当容器当中有值的时候生产线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产:" + n);
this.n = n;
flag = true; // 生产完毕,容器中已经有数据
notifyAll(); // 唤醒所有线程(唤醒消费线程,消费线程消费,唤醒生产线程,生产线程继续等待)
}
public static void main(String[] args) {
Queue queue = new Queue();
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
我们在通过合理运用了wait方法和notify方法避免了消费线程和生产线程同时阻塞的情况,即解决了死锁问题。
输入输出流
- 流就是一连串的字符,以先进先出的方式发送信息的通道
1. File类的使用
什么是文件:
- 文件可认为是相关记录或者放在一起的数据的集合
- 在Java当中,使用java.io.File类对文件进行操作
- 在Windows当中路径是反斜杠\,在Linux当中的路径是正斜杠/
- java程序当中路径是双反斜杠,因为第一个反斜杠是转义用的 ```java
public class FileDemo { public static void main(String[] args) { // 创建File对象(官网有三种构造方法) File file1 = new File(“G:\learnjava\src\com\imooc\file\score.txt”); // 判断是文件还是目录 file1.isDirectory(); // false file1.isFile(); // true
// 判断文件的路径是否是绝对路径
file1.isAbsolute(); // true
// 创建目录(创建单级目录使用mkdir,多级目录使用mkdirs)
File file2 = new File("G:\\learnjava\\src\\com\\imooc\\set");
if(!file2.exists()) { // 路径不存在
file2.mkdir();
}
// 创建文件
if(!file1.exists()) {
try {
file1.createNewFile(); // 创建一个文件
} catch (IOException e) {
e.printStackTrace();
}
}
}
} ```
下面来说一下绝对路径和相对路径:
- 绝对路径,是从盘符开始的路径
- 相对路径:是从当前路径开始的路径(”..\“的意思就是上一级目录,实际开发当中也是相对路径用的多一些)。
2.字节流
- 字节输入流: InputStream
- 字节输出流: OutputStream
关于上述的图,我们了解一下就好,并不会学习所有,我们下面会学习一些重点的东西。