快速入门
计算机的核心作用就是处理数据, 变量用来存储单个数据, 数组用来储存一批数据, 对象用来存储一类数据
什么是对象: 对象就是一种特殊的数据结构, 在java中万物皆对象
面相对象编程的好处: 更加符合人类思维习惯
类和实例对象
在java中必须先设计类, 才能根据类创建对象
类是创建对象的模板, 对象是类的具体实例
语法
成员变量
- 类中的变量称为成员变量, 类中的方法称为成员方法
- 完整定义格式:
- 一般无需指定初始值, 存在默认值
注意事项
- 类名称首字母大写, 满足驼峰模式
- 一个代码文件中, 可以写多个class类, 但只能有一个用public修饰, public修饰的类名必须是java代码的文件名
- 实际开发中, 建议一个文件只定义一个class类
执行原理
执行流程
- java程序运行在jvm虚拟机中
- jvm虚拟机把内存划分为3块, 方法区, 栈内存, 堆内存
- jvm虚拟机执行代码时, 首先把Test启动类提取到方法区中执行
- 然后把Test启动类的main方法放到栈内存中运行
- main方法中使用了Cart类, 虚拟机把Cart类提取到方法区
- new Cart时,会在堆内存中开辟空间, 存放汽车对象, 汽车象会有一个类的地址. 指向方法区的汽车类
- 存放汽车对象的变量地址会被赋值给左侧的c1变量, 对象创建完成
- 对象变量c1通过变量地址, 找到堆内存中的汽车对象, 在找到对象对象的name属性, 完成属性赋值
注意事项:
- 对象与对象之间的数据不会相互影响, 除非多个变量指向同一个对象
- 如果某个对象没有变量引用它, 该对象无法被操作,会被视为垃圾对象, 垃圾对象会被垃圾回收机制清除
构造器
构造器的作用: 定义在类中, 用于创建一个类的对象, 并返回对象的地址
public class Car {
//成员变量
private String name;
//无参构造器
public Car() {
}
//有参构造器
public Car(String n) {
this.name = n;
}
}
// 调用无参构造器
Car c1 = new Car()
// 调用有参构造器
Car c2 = new Car("奔驰")
- 构造器必须与类名一致, 且没有返回值
- 无参构造器: 创建对象, 里面的数据都是默认值
- 有参构造器: 创建对象, 同时可以为里面的数据赋值
- 类在定义时, java会为类自动生成无参构造器
- 如果类中定义了有参构造器, java就不会自动生成无参构造器了, 此时建议手动补充无参构造器
this关键字
可以出现在构造器和成员方法中, 代表当前对象的地址
public class Car {
// this在构造器中使用
public Car() {
sout("this1:" + this)
}
// this在成员方法中使用
public void run() {
sout("this2:" + this)
}
}
作用
this关键字指向当前类的调用者, 用来访问该对象的成员变量和成员方法
解决对象成员变量与方法内部变量可能的命名冲突
三大特性-封装
概念
封装是一种设计对象的原则, 它帮助我们正确的设计对象的属性和方法
好处
让变成变得简单, 有什么事情, 找对象调方法就行了
原则
对象代表什么, 就要封装对应的数据, 并提供数据对应的行为
合理隐藏: 成员变量全部隐藏, 提供get/set方法访问数据
合理暴露: 成员方法根据是否有用对外暴露
修饰符
公开成员: public修饰符, 公开成员可以通过对象直接访问
私有成员: private修饰符, 私有成员无法通过对象访问, 只能在类的内部访问
public class Student {
// 成员变量私有
private double score;
// 成员方法按需暴漏
public void setScore(double score) {
this.score = score;
}
// 通过方法操作数据
public double getScore(double score) {
return score;
}
}
javaBean
javaBean也称为 实体类, 是一种专门保存数据的java类
要求
- 成员变量必须私有(private), 提供getXX/setXX方法
- 必须提供无参构造器, 有参构造器可选
好处
- 实际开发中数据和数据的处理都会很多, 如果都混在一起, 代码的可读性和可维护性都是很差
- 所以流行的开发方式就是数据和数据处理相分离, 所以要用实体类保存数据
static关键字
static是静态的意思, 也就是静态修饰符, 可以用来修饰成员变量, 成员方法
静态变量
// 定义
public class User {
// 静态变量
static String name;
// 实例变量
int age;
}
// 访问方式1(推荐)
// 通过类名访问: 类名.静态成员变量
User.name;
// 访问方式2(不推荐)
// 通过对象访问: 对象.静态成员变量
User u = new User();
// 访问静态变量
u.name;
// 访问实例变量
u.age;
- static修饰的成员变量就是静态变量(类变量)
- 静态变量属于类, 可以被类的所有对象访问
- 推荐通过类名访问静态变量, 因为用过对象访问实例变量, 并不直观
- 无static修饰的成员变量就是实例变量
- 实例变量是属于每个对象的
- 只能通过实例对象访问, 且每个对象的信息不同
特点
- 静态变量只会跟随类加载一次, 内存中只有一份
- 静态成员变量: 某个数据只需要一份, 且希望能够被共享, 该数据就应该定义为类变量
// 系统启动后, 需要用户类记住自己创建了多少个对象
public class User {
// 类变量
public static int number;
// 构造器
public User() {
User.number++;
}
}
静态方法
// 定义
public class User {
// 实例方法
public void run() {
sout("好好学习.天天向上")
}
// 静态方法
public static int getMax(int a, int b) {
return a > b ? a : b
}
}
// 访问方式1(推荐)
// 通过类名访问: 类名.静态成员方法
User.getMax();
// 访问方式2(不推荐)
// 通过对象访问: 对象.静态成员方法
User u = new User();
// 访问静态方法
u.getMax();
// 访问实例方法
u.run();
- 静态成员方法(static修饰, 属于类), 建议使用类名触发, 也可以用对象触发(不直观)
- 实例成员方法(无static修饰, 属于对象),只能用对象触发
- 如果表示对象自己的行为, 且方法中需要访问实例成员的, 则该方法必须声明成实例方法
- 如果该方法是执行公用功能, 则可以声明成静态方法
- 静态方法只能访问静态成员, 不可以直接访问实例成员
- 实例方法可以访问静态成员, 也可以访问实例成员
- 静态方法中不可以使用this关键字
main方法
main方法是类方法, 使用java命令执行Test(类)程序的时候, 虚拟机会通过Test(类)访问main方法, 然后运行
java Test -> Test.main()
工具类
工具类就是一个类中只定义类方法, 每个方法完成一个功能, 这个类就是工具类
好处
- 调用方便: 如果使用实例方法, 还要new出实例, 麻烦而且浪费内存
- 代码复用: 一次编写, 处处可用
- 建议: 工具类不需要创建对象, 建议把工具类的构造器进行私有
public class XxxUtil {
// 方法1
public static void xxx() {
...
}
// 方法2
public static boolean xxx(String email) {
...
}
// 方法3
public static String xxx(int n) {
...
}
// 构造器私有
private XxxUtil() { }
}
代码块
代码块是类的5大成分之一: 成员变量/构造器/方法/代码块/内部类
在java中, 使用 {} 括起来的代码被称为代码块
静态代码块
- 格式: static { }
- 特点: 类加载时自动执行, 由于类只会加载一次, 所以静态代码块也只会执行一次
- 使用: 在类加载的时候做一些静态数据初始化的操作, 例如: 对类变量的初始化赋值
实例代码块(了解)
- 格式: { }
- 特点: 每次创建对象, 调用构造器执行时, 都会执行实例代码块, 并在构造器之前执行
- 使用: 和构造器一样, 用来完成对象的初始化赋值
三大特性-继承
继承就是用extends关键字, 让一个类和另一个建立父子关系
// Student称为子类
// People称为父类
public class Student extends People {
}
- 作用: 子类继承父类后, 就可以直接使用父类公共的属性和方法了
- 好处: 继承的好处就是提高代码的复用性
- 规范: 子类们相同的特性(公共属性/公共方法)放在父类中定义, 子类独有的属性和方法放在子类中定义
原理
子类的对象是由子类和父类共同决定的
权限修饰符
权限修饰符的作用就是 限制类中的成员能够被访问的范围
- 访问权限从小到大是: private(私有)< 缺省(不写) < protected(受限制) < public(公开)
继承的特点
- 子类可以继承父类的属性和方法
- 子类可以继承父类的私有成员, 只是不能直接访问
- 子类不能继承父类的构造器, 因为子类有自己的构造器
- 子类可以直接使用父类的静态成员(共享), 但是共享不等于继承
- jaba是单继承模式: 一个类只能继承一个直接父类
- java不支持多继承, 但是支持多层继承
- java中所有的类都是Object类的子类, 要么直接继承, 要么间接继承
继承后的变化
1.子类访问成员的原则
在子类方法中访问成员(成员变量/成员方法)遵循就进原则
- 先在子类局部范围找, 然后在子类成员范围找, 然后在父类范围找, 找不到就报错
- 如果子父类中出现了重名的成员, 会优先使用子类的(更近)
- 如果一定要在子类中使用父类的成员, 可以通过super关键字,指定访问父类的成员
- 格式: super.父类成员变量/父类成员方法
- 也可以通过this关键字, 指定访问子类的成员 (没用, 因为默认就访问子类的成员)
- 格式: this.成员变量/成员方法
2.子类的方法重写
当子类觉得父类中的某个方法不好用, 子类可以重写一个名称和参数列表相同的方法, 去覆盖父类的方法
- 重写后, 方法的访问, java会遵循就近原则
- 建议使用@Override注解, 他可以让编译器检查重写的格式, 使代码可读性更好
基本要求
- 重写方法的名称和形参列表必须与被重写方法保持一致
- 私有方法, 静态方法不能被重写
- 子类重写父类方法, 访问权限必须等于或大于父类该方法的权限
- 重写的方法的返回值, 必须与被重写方法的返回值一致,或者范围更小
- 以上了解即可, 实际开中遵循: 声明不变, 重新实现
举例
- 可以通过重写Object类的toString方法, 使其返回对象内容, 而不是地址
- 可以通过右键->Generate->toString 快速生成
3.子类构造器的特点
子类中所有的构造器, 默认都会先调用父类的无参构造器, 再执行自己
原因
- 子类在初始化的时候,有可能会用到父类中的数据, 如果父类没有初始化, 子类就无法使用父类的数据
- 所以, 子类初始化之前, 一定会调用父类的构造器, 完成父类的初始化
- 默认情况下, 子类构造器的第一行代码都是super() , 写不写都有, 他会调用父类的无参数构造器
- 如果父类没有无参构造器, 则会报错, 我们需要在子类调用父类的有参构造器, super(参数)
- 在继承模式下, 处理数据的构造器被分到多个不同的类中去了,
- 所以需要先调用父类构造器, 再调用自己的构造器, 确保数据的完整初始化
4.子类其他构造器
如果父类中没有无参构造器, 代码就会报错, 因为子类默认要调用父类的无参构造器, 我们可以通过 super(...) 调用父类的有参构造器, 解决问题
// 父类
public class People {
private String name;
// 有参构造器
public Prople(String name) {
this.name = name
}
}
// 子类
public class Student extends People {
private int age;
// 有参构造器
public Student(int age) {
// 调用兄弟构造器
this(age, '张三');
}
// 有参构造器
public Student(int age, String name) {
// 调用父类有参构造器
super(name)
// 初始化对象
this.age = age;
}
}
// 初始化学生对象
Student s = new Student(18)
注意
- 子类通过 this(...) 调用本类的其他构造器, 其他构造器可以通过 super(...) 去调用父类的构造器
- super(...) 和 this(...) 都只能放在构造器的第一行, 所以不能同时使用
this和super
this代表本类对象的引用, super代表父类存储空间的标识
包
包就是分门别类的管理类的, 类似于文件夹
建包
// 语句
// package 公司域名倒写.技术名称
// 必须在第一行, 一般IEDA工具会帮助创建
package com.itheima.javabean;
public class Student {
}
导包
相同包下的类可以直接访问, 不同包下的类必须导包
- 语句: import 包名.类名;
- 使用java官方的程序, 也需要导包, lang包下的程序可以直接用
- 一个类中使用多个不同包下的类, 如果类的名字一样, 默认只能导入一个, 另一个必须带包名访问
自动导包
权限修饰符
用来控制一个成员能够被访问的范围, 可以修饰成员变量, 构造器, 内部类
- 能够识别他人定义的成员的访问范围
- 自己定义成员一般要满足如下要求:
- 成员变量一般私有
- 方法一般公开
- 希望该成员只能被本类访问, 使用private修饰
- 希望该成员只能被本类和同一个包下的其他类及子类访问, 使用protected修饰
- 其他情况用public就行
final关键字
final关键字是最终的意思, 可以修饰类/方法/变量
- 修饰类: 该类就是最终类, 特点是不能被继承, 工具类中可能会用到
- 修饰方法: 该方法就是最终方法, 特点是不能被重写
- 修饰变量: 该变量就只能被赋值一次,不能再次赋值
- 基本类型的变量: 该变量存储的数据不能被改变,
- 引用类型的变量: 该变量存储的地址不能被改变, 但是对象里面的值可以改变
常量
使用public static final 修饰的成员变量就是常量, 必须要有初始值
- 命名规范: 全大写英文, 使用下划线连接
- 作用: 通常用于记录系统的配置信息
- 优势:
- 实现软编码, 代码可读性更好,可维护性更好
- 程序编译后,常量会被"宏替换", 即出现常量的地方全部会被替换成其记住的字面量,保证常量和字面量的性能是一样的
枚举
枚举是java中一种特殊的类, 专门用来做信息分类, 可读性好, 入参约束谨慎, 代码优雅
定义
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
反编译后
- 枚举类都是继承了枚举类型: java.lang.Enum
- 枚举都是最终类, 不可以被继承
- 枚举类的构造器都是私有的, 枚举对外不能创建对象
- 枚举类的第一行只 能罗列一些名称, 这些名称都是常量, 并且每个常量记住的都是枚举类的一个对象
- 枚举类中, 从第二行开始, 可以定义类的其他各种成员
- 枚举类相当于是多例模式
- 枚举会从 java.lang.Enum 类中继承一些方法
- Season[] se = Season.values() // 拿到全部对象
- Season se = Season.valueOf("SPRING"); // 拿到指定名称的枚举对象
- se.name() // 拿到枚举对象的名字
- se.ordinal() // 拿到枚举对象的索引
补充
- 可以使用枚举实现单例模式
public enum C {
x; //单例
}
- 枚举和常量都是用来表示一组信息, 然后作为参数进行传输
- 枚举相对于常量, 限制性更好,
- 常量相对于枚举, 灵活性更好,
- 两者的使用场景相似
抽象类
abstract关键字是抽象的意思, 可以修饰类, 成员方法
// 抽象类
修饰符 abstract class 类名 {
// 抽象方法
修饰符 abstract 返回值类型 方法名称(形参列表);
}
- 抽象方法只有方法签名, 不能声明方法体
- 一个类中如果定义了抽象方法,这个类必须声明为抽象类, 否则报错
- abstract修饰的类称为抽象类, abstract修饰的方法称为抽象方法
- 类该有的成员(成员变量/方法/构造器),抽象类都可以有
- 抽象类可以理解为不完整的设计图, 得到了抽象方法, 但是失去了创建对象的能力
- 所以抽象类不能创建对象, 仅作为一种特殊的类, 让子类继承并实现抽象方法
- 一个类继承抽象类, 必须重写完抽象类的全部抽象方法, 否则这个类也要定义成抽象类
- abstract不能修饰变量, 代码块, 构造器
示例
父类知道每个子类都要做某个行为, 但每个子类要做的情况不一样
- 父类就可以定义抽象方法, 交给子类去重写实现,
- 设计这样的抽象类, 就是为了更好的支持多态
扩展
- final和abstract是互斥关系
- abstract定义的抽象类作为模板让子类继承, final定义的类不能被继承
- 抽象方法定义通用功能让子类重写, final定义的方法子类不能重写
- 使用抽象类可以实现 [模版方法] 设计模式
三大特性-多态
多态就是对象可以有多种形态, 是继承/实现情况下的一种现象, 表现为:对象多态和行为多态
对象多态: 一个对象可以有角色(形态), 你既是你爸爸的儿子, 也是你媳妇的老公
行为多态: 一个对象的行为可以有不同的表现, 人都会唱歌, 但是你唱的和刘德华唱的不一样
注意: 多态是对象或行为的多态, java中的属性(成员变量)不谈多态
多态的前提
- 有继承/实现关系;
- 存在父类引用子类对象;
- 存在方法重写 (多态侧重行为多态)
常见形式
父类类型 对象名称 = new 子类构造器
public class Prople {
public String name = "父类的名称"
public void run() {
sout("人可以跑")
}
}
public class Teacher extends Prople {
public String name = "老师的名称"
public void run() {
sout("老师跑的气喘吁吁")
}
}
public class Student extends Prople {
public String name = "学生的名称"
public void run() {
sout("学生跑的飞快")
}
}
public class Test {
public static void main(String[] ages) {
// 对象多态: 人对象可以指向学生对象,也可以指向老师对象
People p1 = new Student();
People p2 = new Teacher();
// 行为多态: 同样调用run方法,执行结果不同
p1.run(); // 学生跑的飞快
p2.run(); // 老师跑的气喘吁吁
// 成员变量不存在多态
p1.name; // 父类的名称
p1.name; // 父类的名称
}
}
运行特点
- 方法调用: 编译看左边(People), 运行看右边(Student();)
- 变量调用: 编译看左边(People), 运行也看左边(People)
多态的好处
1:在多态形势下, 右边对象是解耦合的, 更便于扩展和维护
// 可以方便的切换
// prople p1 = new Teacher();
prople p1 = new Student();
p1.run()
2:定义方法时, 使用父类类型的形参, 可以接受一切子类对象, 扩展性更强
Student s = new Student();
go(s);
Teacher t = new Teacher();
go(t);
publci static void go(People p) {
}
多态的问题
多态下不能使用子类的独有功能
public class Prople {
public void run() {
sout("人可以跑")
}
}
public class Teacher extends Prople {
public void run() {
sout("老师跑的气喘吁吁")
}
public void sayHi() {
sout("老师需要讲课")
}
}
public class Test {
public static void main(String[] ages) {
People p1 = new Student();
p1.run(); // 老师跑的气喘吁吁
p1.sayHi(); // 报错
}
}
解决方法
强制类型转换: 把父类类型强转成子类类型, 解决多态下无法使用子类独有功能的问题
方法: 子类 变量名 = (子类) 父类变量
public class Test {
public static void main(String[] ages) {
People p1 = new Student();
// 强制类型转换
Student s1 = (Student) p1;
// 正常运行: "老师需要讲课"
p1.sayHi();
// 类型谈判, 防止类型转换错误
if(p1 instanceof Student) {
Student s1 = (Student) p1;
p1.sayHi();
}
}
}
补充
- 只要存在继承/实现关系, 就可以在编译阶段进行强制类型转换, 编译阶段不会报错
- 运行时, 如果发现强转的类型和对象的真实类型不符, 就会报类型转换异常(ClassCastException)的错误
- 官方建议, 强制类型转换前先 使用 instanceof 关键字, 判断当前对象的真实类型
接口
接口是一种规范, 在java中, 使用 interface关键字定义接口, 可以理解为一种特殊的结构
定义
public interface A {
// 成员变量(默认就是常量)
String SCHOOL_NAME = "程序员";
// 成员方法(默认就是抽象方法)
void test();
}
- JDK8之前的接口中只能有抽象方法和常量
- 接口中的成员都是public修饰的, 写不写都是
- 接口方法默认被public修饰的原因: 接口是需要被类实现的, 大部分接口方法都是要对外暴漏的
- 接口不能实例化
使用
接口不能创建对象, 接口是用来被 类 实现的, 实现接口的类称为实现类
// 实现接口的关键字: implements
修饰符 class 实现类 implements 接口1, 接口2, ... {
}
- 一个类可以实现多个接口(干爹)
- 一个类实现接口, 必须重写完 全部接口的 全部抽象方法, 否则实现类需要定义为抽象类
优势
- 弥补了类单继承的不足,一个类可以同时实现多个接口
- 面向接口编程, 可以灵活方便的切换各种业务实现
新增
jdk8开始, 接口新增了三种方法, 目的是增强接口的能力, 更便于项目的扩展和维护
了解即可, java源码中会看到
1.默认方法
// 默认方法必须使用default修饰
// 默认方法, 使用实现类的对象调用
default void run() {
sout("默认方法")
}
2.静态方法
// 静态方法必须使用static修饰
// 静态方法, 使用本身的接口名来调用
static void run() {
sout("静态方法")
}
3.私有方法
// 私有方法必须使用private修饰
// jdk9开始支持, 只能在本类中被其他的默认方法或私有方法访问
static void run() {
sout("私有方法")
}
接口的多继承
一个接口可以同时继承多个接口, 便于实现类实现多个接口
public interface 接口1 extends 接口1, 接口2 {
}
接口使用细节
一个接口继承多个接口, 如果多个接口中存在方法签名冲突, 则此时不支持多继承
一个类实现多个接口, 如果多个接口中存在方法签名冲突, 则此时不支持多实现
一个类继承了父类, 又同时实现了接口, 父类中和接口中存在同名的默认方法, 实现类会优先使用父类的
一个类实现了多个接口, 多个接口中存在同名的默认方法, 会冲突, 解决方法就是在实现类中重写该方法
内部类
如果一个类中定义在另一个类的内部, 这个类就是内部类
内部类是类的五大成分之一(成员变量/方法/构造器/内部类/代码块)
public class People {
// 内部类
public class Heart {
}
}
- 场景: 当一个类的内部, 包含了一个完成的事物, 且这个事物没必要单独设计时, 就可以设计成内部类
- 内部类通常可以方便访问外部类的成员, 包括私有成员
- 内部类提供了更好的封装性, 可以使用private, protectecd修饰内部类
静态内部类
用static修饰的内部类, 属于外部类自己持有, 特点与普通类一致
// 定义
public class Car {
// 静态成员内部类
public static class Engine { }
}
// 创建对象
Car.Engine eg = new Car.Engine();
// 访问成员
// 可以直接访问外部类的静态成员
// 不可以直接访问外部类的实例成员
成员内部类
无static修饰, 属于外部类的对象, JDK16之后, 成员内部类也可以定义静态成员了
// 定义
public class Car {
// 成员内部类
public class Engine { }
}
// 创建对象
Car.Engine eg = new Car().new Engine();
// 访问成员
// 可以直接访问外部类的实例成员和静态成员
// 可以拿到当前外部类对象, 格式是: 外部类名.this
匿名内部类(重点)
本质是一个没有名字的局部内部类, 作用就是方便的创建一个子类对象, 目的是简化代码
// 1.定义一个抽象类
abstract class Animal {
public abstract void cry();
}
// 2.使用匿名内部类语法
// 把匿名内部类编译成一个子类, 然后立即创建一个子类对象
Animal a = new Animal(){
@Override
public void cry() {
sout("喵喵喵")
}
};
// 直接使用子类
a.run(); // 喵喵喵
- 语法:
- 匿名内部类本质是一个子类, 并且会立即创建一个子类对象
- 匿名内部类的对象类型, 相当于是当前new的那个类型子类类型
- 使用场景: 把匿名内部类当做参数传给方法
- 匿名内部类都是在需要的时候用, 比如一个方法需要一个对象作为参数
- 那么单独创建对象传进去就麻烦, 此时可以使用匿名内部类简化代码
泛型
泛型提供了在编译阶段的类型约束, 并自动进行检查, 可以避免强制类型转换和可能出现的转换异常
// 该集合只能添加字符串类型的数据
ArrayList<String> list1 = new ArrayList<>();
list1.add("java1");
- 定义类,接口,方法时, 同时声明一个或多个类型变量,用于限制成员类型,
- 使用泛型的类称为泛型类, 泛型接口或泛型方法, 统称为泛型
- 原理: 把具体的数据类型作为参数传给类型变量, 通过类型变量限制成员的类型
自定义泛型类
// 模拟ArrayList集合
public class MyArrayList<E> {
// 模拟add方法
public boolean add(E e) {
return true;
}
// 模拟get方法
public E get(int index) {
return null;
}
}
// 使用泛型
MyArrayList<String> list = new MyArrayList<>();
list.add("java1")
list.get(1)
- 如果需要, 可以进一步使用extends限制泛型的范围
public class MyArrayList<E extends Animal> { }
- 表示类型E必须是Animal类型或者是Animal的子类
自定义泛型接口
public interface Date<T> {
void add(T t);
ArrayList<T> getByName(String name):
}
- 类型变量建议使用大写英文字母, E T K V 等
自定义泛型方法
public static <T> T test(T t) {
return t;
}
通配符
- 在使用泛型是可以用 ? 表示一切类型
泛型上下限
- 泛型上限: ? extends Cart
- 表示能接受的类型必须是Car或其子类
- 泛型下限: ? super Cart
- 表示能接受的必须是Cart或其父类
泛型擦除
- 泛型是在编译阶段工作的. 就是帮助程序进行类型校验的, 程序编译成class文件后, 泛型就不会存在了
- 泛型不支持基本数据类型, 只支持引用数据对象, 如Integer Double