android的第一个app项目(java版)
一.学习java重要概念
java的基本类型的语言方法和C语言很像,这都是我们要学的东西和学过的东西。那些基础东西,就不和大家讨论了,一起看一下java的一些知识架构。
1.封装
封装是面向对象编程中的一个核心概念,它涉及到将数据和操作数据的方法结合在一起,形成一个整体,即类。在封装的过程中,类的内部实现对外部是隐藏的,只有通过定义好的接口才能与类的对象进行交互。这样做的目的是为了保护对象内部的数据不被外部随意访问和修改,确保数据的安全性和程序的健壮性。
一句话:将方法和字段一起包装到一个单元中,单元以类的形式实现。 类比C语言:
struct student{
//特征
int age;
char* name;
double score;
//行为
void (*introduce)(int age,char* name,double score);
void (*testFunc)();
}
如何实现封装: 在Java中,封装通常是通过将类的字段(属性)设置为私有(private)来实现的,然后通过公共(public)的getter和setter方法来访问和修改这些字段。例如:
public class Person {
private String name;
private int 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;
}
}
在上述代码中,name和age字段被设置为私有的,外部代码不能直接访问这些字段,而是必须通过getName、setName、getAge和setAge方法来进行访问和修改。
封装的实际应用: 在实际开发中,封装可以帮助我们隐藏实现细节,只暴露必要的操作接口,这样即使内部实现发生变化,也不会影响到使用该类的其他代码。例如,如果我们有一个BankAccount类,我们可以隐藏账户的余额,只提供存款和取款的方法:
public class BankAccount {
static double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
}
}
public double getBalance() {
return balance;
}
}
public class getmoney {
BankAccount stu1 = new BankAccount();
stu1.balance=7;
stu1.deposit(100);
stu1.witrhdraw(99);
stu1.getBalance();
System.out.println("balance="+stu1.balance);
}
类-----模板
类不能直接使用,不能直接访问变量,需要先实例化,申请一个空间
2.封装的访问修饰符
位置 | private | 默认 | protected | public |
同一个类 | 是 | 是 | 是 | 是 |
同一个包内的类 | 否 | 是 | 是 | 是 |
不同包内的子类 | 否 | 否 | 是 | 是 |
不同包并且不是子类 | 否 | 否 | 否 | 是 |
public:该类或非该类均可以访问
private:只有该类可以访问
protected:该类及其子类的成员可以访问,同一个包中的类可以访问
默认:同一个包中的类可以访问
pubilc class year{
private int Year;
private boolean isLeapYear(){
if((year%4==0&&year%100!=0||year%400==0)){
return true;
}else{
return false;
}
}
}
3.UML类图及构造方法
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
+:表示public
-:表示private
#:表示protected
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
注意:构造方法的调用,特别是new操作符(实例化对象的时候,自动被调用)
规则:(1)构造方法名和类名一致(2)没有返回类型(3)方式实现主要为字段赋初值
public class People {
private String name;
private int age;
//无参构造
public People() {
System.out.println("一个人的诞生");
}
//有参构造方法
public People(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
People people = new People();
}
}
构造方法和方法是类似的,他也可以重载。其重载的方法很简单,就是只要提供不一样的参数即可,编译器就会去通过不同的参数找到对应的构造方法
public class People {
private String name;
private int age;
private String sex;
//无参构造
public People() {
System.out.println("一个人的诞生");
}
//有参构造方法
public People(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public People(String name) {
this.name = name;
}
public static void main(String[] args) {
People people = new People("张三",18,"女");
People people2 = new People("小帅",2);
People people3 = new People("老王");
//姓名null年龄0
//System.out.println("姓名" + people.name + "年龄" + people.age);
}
}
4.this关键字
在Java编程语言中,this关键字是一个非常重要的概念,它代表当前对象的引用。这个关键字可以在类的实例方法和构造器中使用,主要有三种用法:
用法一:引用当前类的属性或方法
当类的构造器或方法中有与类的成员变量同名的局部变量时,可以使用this来明确指代成员变量。例如,在一个账户类Account中,构造器接收name、balance和pwd作为参数,为了避免与成员变量混淆,可以使用this来引用成员变量:
public class Account {
private String name;
private double balance;
private String pwd;
public Account(String name, double balance, String pwd) {
this.name = name;
this.balance = balance;
this.pwd = pwd;
}
}
在上述代码中,this.name、this.balance和this.pwd确保了构造器中的参数正确地初始化了对象的成员变量,而不是简单地赋值给了局部变量。
用法二:调用当前类的方法
this关键字还可以用来调用另外一个方法,如果没有使用的话。例如:
public class InvokeCurrentClassMethod {
public InvokeCurrentClassMethod() {
}
void method1() {
}
void method2() {
this.method1();
}
public static void main(String[] args) {
(new InvokeCurrentClassMethod()).method1();
}
}
用法三:返回当前类的实例
this关键字还可以用于返回当前类的实例,这在链式编程中非常有用,可以使得方法调用可以连续进行。例如:
public class ChainExample {
private int value;
public ChainExample setValue(int value) {
this.value = value;
return this; // 返回当前对象的引用,用于链式调用
}
public void display() {
System.out.println("Value: " + this.value);
}
}
public class Main {
public static void main(String[] args) {
ChainExample example = new ChainExample();
example.setValue(10).display(); // 方法链式调用
}
}
具体实例:当没有调用this关键字
public class WithoutThisStudent {
String name;
int age;
WithoutThisStudent(String name, int age) {
name = name;
age = age;
}
void out() {
System.out.println(name+" " + age);
}
public static void main(String[] args) {
WithoutThisStudent s1 = new WithoutThisStudent("沉默王二", 18);
WithoutThisStudent s2 = new WithoutThisStudent("沉默王三", 16);
s1.out();
s2.out();
}
}
从结果中可以看得出来,尽管创建对象的时候传递了参数,但实例变量并没有赋值。这是因为如果构造方法中没有使用 this 关键字的话,name 和 age 指向的并不是实例变量而是参数本身。
null 0 null 0
当调用了this关键字
public class WithThisStudent {
String name;
int age;
WithThisStudent(String name, int age) {
this.name = name;
this.age = age;
}
void out() {
System.out.println(name+" " + age);
}
public static void main(String[] args) {
WithThisStudent s1 = new WithThisStudent("沉默王二", 18);
WithThisStudent s2 = new WithThisStudent("沉默王三", 16);
s1.out();
s2.out();
}
}
这次,实例变量有值了,在构造方法中,
this.xxx
指向的就是实例变量,而不再是参数本身了。沉默王二 18 沉默王三 16
调用当前类的构造方法
public class InvokeConstrutor {
InvokeConstrutor() {
System.out.println("hello");
}
InvokeConstrutor(int count) {
this();
System.out.println(count);
}
public static void main(String[] args) {
InvokeConstrutor invokeConstrutor = new InvokeConstrutor(10);
}
}
在有参构造方法
InvokeConstrutor(int count)
中,使用了this()
来调用无参构造方法InvokeConstrutor()
。 来看一下输出结果。这个this()调用当前类的构造方法,必须放在第一位。hello 10
5.static关键字
虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。
5.1 静态变量
static变量也称为静态变量,静态变量和非静态变量的区别:
静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化
非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响
public class Student {
String name;//非静态变量
int age;//非静态变量
static String school = "郑州大学";//静态变量
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Student s1 = new Student("沉默王二", 18);
Student s2 = new Student("沉默王三", 16);
}
}
public class Counter {
int count = 0;
Counter() {
count++;
System.out.println(count);
}
public static void main(String args[]) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
}
}
IDEA 中点了一下运行按钮,程序跑了起来。
1
1
1
public class StaticCounter {
static int count = 0;
StaticCounter() {
count++;
System.out.println(count);
}
public static void main(String args[]) {
StaticCounter c1 = new StaticCounter();
StaticCounter c2 = new StaticCounter();
StaticCounter c3 = new StaticCounter();
}
}
“来看一下输出结果。”
1
2
3
解释一下,由于静态变量只会获取一次内存空间,所以任何对象对它的修改都会得到保留,所以每创建一个对象,count 的值就会加 1,所以最终的结果是 3,明白了吧。
5.2 静态方法
非静态方法,当其他类调用非静态方法的时候,要new一下实例化。
public class StaticMethodStudent {
String name;
int age;
static String school = "郑州大学";
public StaticMethodStudent(String name, int age) {
this.name = name;
this.age = age;
}
static void change() {
school = "河南大学";
}
void out() {
System.out.println(name + " " + age + " " + school);
}
public static void main(String[] args) {
StaticMethodStudent.change();
StaticMethodStudent s1 = new StaticMethodStudent("沉默王二", 18);
StaticMethodStudent s2 = new StaticMethodStudent("沉默王三", 16);
s1.out();
s2.out();
}
}
“来看一下程序的输出结果吧。
沉默王二 18 河南大学 沉默王三 16 河南大学
5.3 静态块区
静态初始化块可以置于类中的任何地方,类中可以有多个静态初始化块。
在类初次被加载时,会按照静态初始化块的顺序来执行每个块,并且只会执行一次。
public class StaticBlock {
static {
System.out.println("静态代码块");
}
public static void main(String[] args) {
System.out.println("main 方法");
}
}
构造方法用于对象的初始化。
静态初始化块,用于类的初始化操作。
在静态初始化块中不能直接访问非staic成员。
“来看一下程序的输出结果吧。”
静态代码块 main 方法
静态代码块通常用来初始化一些静态变量,它会优先于 main()
方法执行。
public class StaticBlockNoMain {
static {
System.out.println("静态代码块,没有 main");
}
}
“在命令行中执行 java StaticBlockNoMain
的时候,会抛出 NoClassDefFoundError 的错误。”
6.super关键字
“super 关键字的用法主要有三种。”
- 指向父类对象;
- 调用父类的方法;
super()
可以调用父类的构造方法。
6.1 指向父类对象
1.如果父类和子类拥有同样名称的字段,super 关键字可以用来访问父类的同名字段。
public class ReferParentField {
public static void main(String[] args) {
new Dog().printColor();
}
}
class Animal {
String color = "白色";
}
class Dog extends Animal {
String color = "黑色";
void printColor() {
System.out.println(color);
System.out.println(super.color);
}
}
“父类 Animal 中有一个名为 color 的字段,子类 Dog 中也有一个名为 color 的字段,子类的
printColor()
方法中,通过 super 关键字可以访问父类的 color。”“来看一下输出结果。”
黑色 白色
6.2 指向父类构造方法
2.父类 Animal 和子类 Dog ,在子类Dog中,可以创建一个构造方法,在构造方法中调用父类的构造方法,但是super()。必须放在首行。
public class ReferParentConstructor {
public static void main(String[] args) {
new Dog();
}
}
class Animal {
Animal(){
System.out.println("动物来了");
}
}
class Dog extends Animal {
Dog() {
super();
System.out.println("狗狗来了");
}
}
来看一下输出结果。
动物来了 狗狗来了
在默认情况下,super()
是可以省略的,编译器会主动去调用父类的构造方法。也就是说,子类即使不使用 super()
主动调用父类的构造方法,父类的构造方法仍然会先执行。
public class ReferParentConstructor {
public static void main(String[] args) {
new Dog();
}
}
class Animal {
Animal(){
System.out.println("动物来了");
}
}
class Dog extends Animal {
Dog() {
System.out.println("狗狗来了");
}
}
“输出结果和之前一样。”
动物来了 狗狗来了
6.3 指向父类有参构造方法
3.super()
也可以用来调用父类的有参构造方法,这样可以提高代码的可重用性
class Person {
int id;
String name;
Person(int id, String name) {
this.id = id;
this.name = name;
}
}
class Emp extends Person {
float salary;
Emp(int id, String name, float salary) {
super(id, name);
this.salary = salary;
}
void display() {
System.out.println(id + " " + name + " " + salary);
}
}
public class CallParentParamConstrutor {
public static void main(String[] args) {
new Emp(1, "沉默王二", 20000f).display();
}
}
“Emp 类继承了 Person 类,也就继承了 id 和 name 字段,当在 Emp 中新增了 salary 字段后,构造方法中就可以使用
super(id, name)
来调用父类的有参构造方法。”“来看一下输出结果。”
1 沉默王二 20000.0
7.final关键字
7.1 final 变量
1.被 final 修饰的变量无法重新赋值。换句话说,final 变量一旦初始化,就无法更改。
final int age = 18;
public class Pig {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
我们创建一个测试类,并声明一个 final 修饰的 Pig 对象。如果尝试将 pig 重新赋值的话,编译器同样会生气。
final Pig pig = new Pig();
但我们仍然可以去修改 pig 对象的 name。
final Pig pig = new Pig();
pig.setName("特立独行");
System.out.println(pig.getName()); // 特立独行
另外,final 修饰的成员变量必须有一个默认值,否则编译器将会提醒没有初始化。
final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写。
public class Pig {
private final int age = 1;
public static final double PRICE = 36.5;
}
有时候,我们还会用 final 关键字来修饰参数,它意味着参数在方法体内不能被再修改。如果尝试去修改它的话,编译器会提示以下错误。
public class ArgFinalTest {
public void arg(final int age) {
}
public void arg1(final String name) {
}
}
7.2 final 方法
被 final 修饰的方法不能被重写。如果我们在设计一个类的时候,认为某些方法不应该被重写,就应该把它设计成 final 的。
public class Actor {
public final void show() {
}
}
当我们想要重写该方法的话,就会出现编译错误。
7.3 final 类
任何尝试从 final 类继承的行为将会引发编译错误。
public final class Writer {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
尝试去继承它,编译器会提示以下错误,Writer 类是 final 的,无法继承。
8.继承
继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法。
我们来举个例子:动物有很多种,是一个比较大的概念。在动物的种类中,我们熟悉的有猫(Cat)、狗(Dog)等动物,它们都有动物的一般特征(比如能够吃东西,能够发出声音),不过又在细节上有区别(不同动物的吃的不同,叫声不一样)。
在 Java 语言中实现 Cat 和 Dog 等类的时候,就需要继承 Animal 这个类。继承之后 Cat、Dog 等具体动物类就是子类,Animal 类就是父类。
如果仅仅只有两三个类,每个类的属性和方法很有限的情况下确实没必要实现继承,但事情并非如此,事实上一个系统中往往有很多个类并且有着很多相似之处,比如猫和狗同属动物,或者学生和老师同属人。各个类可能又有很多个相同的属性和方法,这样的话如果每个类都重新写不仅代码显得很乱,代码工作量也很大。
这时继承的优势就出来了:可以直接使用父类的属性和方法,自己也可以有自己新的属性和方法满足拓展,父类的方法如果自己有需求更改也可以重写。这样使用继承不仅大大的减少了代码量,也使得代码结构更加清晰可见。
所以这样从代码的层面上来看我们设计这个完整的 Animal 类是这样的:
class Animal
{
public int id;
public String name;
public int age;
public int weight;
//构造方法来初始化对象
public Animal(int id, String name, int age, int weight) {
this.id = id;
this.name = name;
this.age = age;
this.weight = weight;
}
//这里省略get set方法
public void sayHello()
{
System.out.println("hello");
}
public void eat()
{
System.out.println("I'm eating");
}
public void sing()
{
System.out.println("sing");
}
}
而 Dog,Cat,Chicken 类可以这样设计:
class Dog extends Animal//继承animal
{
public Dog(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
}
class Cat extends Animal{
public Cat(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
}
class Chicken extends Animal{
public Chicken(int id, String name, int age, int weight) {
super(id, name, age, weight);//调用父类构造方法
}
//鸡下蛋
public void layEggs()
{
System.out.println("我是老母鸡下蛋啦,咯哒咯!咯哒咯!");
}
}
public class CallParentParamConstrutor {
public static void main(String[] args) {
new Chicken(1,"蔡徐坤", 20000,18000).layEggs();
}
}
各自的类继承 Animal 后可以直接使用 Animal 类的属性和方法而不需要重复编写,各个类如果有自己的方法也可很容易地拓展。
注意:父类的private属性,会被继承并且初始化在子类父对象中,只不过对外不可见。
子类继承了父类的所有属性和方法或子类拥有父类的所有属性和方法是对的,只不过父类的私有属性和方法,子类是无法直接访到的。
9.方法重载
第一,改变参数的数目。
public class OverloadingByParamNum {
public static void main(String[] args) {
System.out.println(Adder.add(10, 19));
System.out.println(Adder.add(10, 19, 20));
}
}
class Adder {
static int add(int a, int b) {
return a + b;
}
static int add(int a, int b, int c) {
return a + b + c;
}
}
第二,通过改变参数类型,也可以达到方法重载的目的。
public class OverloadingByParamType {
public static void main(String[] args) {
System.out.println(Adder.add(10, 19));
System.out.println(Adder.add(10.1, 19.2));
}
}
class Adder {
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
}
第三.改变参数的数目和类型都可以实现方法重载,为什么改变方法的返回值类型就不可以呢?
编译时报错优于运行时报错,所以当两个方法的名字相同,参数个数和类型也相同的时候,虽然返回值类型不同,但依然会提示方法已经被定义的错误。
注意:方法的返回值只是作为方法运行后的一个状态,它是保持方法的调用者和被调用者进行通信的一个纽带,但并不能作为某个方法的‘标识’。
第四.Java 虚拟机在运行的时候只会调用带有 String 数组的那个 main()
方法。
public class OverloadingMain {
public static void main(String[] args) {
System.out.println("String[] args");
}
public static void main(String args) {
System.out.println("String args");
}
public static void main() {
System.out.println("无参");
}
}
第一个
main()
方法的参数形式为String[] args
,是最标准的写法;第二个main()
方法的参数形式为String args
,少了中括号;第三个main()
方法没有参数。String[] args
第五.由于可以通过改变参数类型的方式实现方法重载,那么当传递的参数没有找到匹配的方法时,就会发生隐式的类型转换。
public class OverloadingTypePromotion {
void sum(int a, long b) {
System.out.println(a + b);
}
void sum(int a, int b, int c) {
System.out.println(a + b + c);
}
public static void main(String args[]) {
OverloadingTypePromotion obj = new OverloadingTypePromotion();
obj.sum(20, 20);
obj.sum(20, 20, 20);
}
}
“执行 obj.sum(20, 20)
的时候,发现没有 sum(int a, int b)
的方法,所以此时第二个 20 向上转型为 long,所以调用的是 sum(int a, long b)
的方法。”
“再来看一个示例。”
public class OverloadingTypePromotion1 {
void sum(int a, int b) {
System.out.println("int");
}
void sum(long a, long b) {
System.out.println("long");
}
public static void main(String args[]) {
OverloadingTypePromotion1 obj = new OverloadingTypePromotion1();
obj.sum(20, 20);
}
}
执行
obj.sum(20, 20)
的时候,发现有sum(int a, int b)
的方法,所以就不会向上转型为 long。来看一下程序的输出结果。int
当有两个方法 sum(long a, int b)
和 sum(int a, long b)
,参数个数相同,参数类型相同,只不过位置不同的时候,会发生什么呢?
不明确,编译器会很为难,究竟是把第一个 20 从 int 转成 long 呢,还是把第二个 20 从 int 转成 long,智障了!所以,不能写这样让编译器左右为难的代码。
10.方法重写
- 重写的方法必须和父类中的方法有着相同的名字;
- 重写的方法必须和父类中的方法有着相同的参数;
- 必须是 is-a 的关系(继承关系)。
public class Bike extends Vehicle {
@Override
void run() {
System.out.println("自行车在跑");
}
public static void main(String[] args) {
Bike bike = new Bike();
bike.run();
}
}
class Vehicle {
void run() {
System.out.println("车辆在跑");
}
}
在方法重写的时候,IDEA 会建议使用 @Override
注解,显式的表示这是一个重写后的方法,尽管可以缺省。
10.1规则一:只能重写继承过来的方法。
因为重写是在子类重新实现从父类继承过来的方法时发生的,所以只能重写继承过来的方法,这很好理解。这就意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 修饰的方法无法被重写。
public class Animal {
public void move() { }
protected void eat() { }
void sleep(){ }
}
//Dog 类来重写这三个方法:
public class Dog extends Animal {
public void move() { }
protected void eat() { }
void sleep(){ }
}
但如果父类中的方法是 private 的,就行不通了。因为父类的 move()
方法是 private 的,对子类并不可见。
public class Dog extends Animal {
public void move() { }
}
10.2 规则二:final、static 的方法不能被重写。
class Animal {
public final void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
// 下面这行代码会报错,不能重写父类的 final 方法
// public void eat() {
// System.out.println("Dog is eating");
// }
}
一个方法是 final 的就意味着它无法被子类继承到,所以就没办法重写。
public class Animal {
final void move() { }
}
static
方法是属于类本身的,而不是某个对象的实例,因此,它的行为与实例方法不同。虽然
static
方法也可以在子类中定义一个同名的方法,但它并不会覆盖父类中的static
方法,因此 静态方法不能被真正的重写。
class Animal {
public static void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public static void eat() {
System.out.println("Dog is eating");
}
}
public class Test {
public static void main(String[] args) {
Animal.eat(); // 输出:Animal is eating
Dog.eat(); // 输出:Dog is eating
}
}
10.3 规则三:重写的方法必须有相同的参数列表。
public class Animal {
void eat(String food) { }
}
Dog 类中的 eat()
方法保持了父类方法 eat()
的同一个调调,都有一个参数——String 类型的 food。
public class Dog extends Animal {
public void eat(String food) { }
}
10.4 规则四:重写的方法必须返回相同的类型。
父类没有返回类型:
public class Animal {
void eat(String food) { }
}
子类尝试返回 String:
public class Dog extends Animal {
public String eat(String food) {
return null;
}
}
于是就编译出错了(返回类型不兼容)。
10.5 规则五:重写的方法不能使用限制等级更严格的权限修饰符。
- 如果被重写的方法是 default,那么重写的方法可以是 default、protected 或者 public。
- 如果被重写的方法是 protected,那么重写的方法只能是 protected 或者 public。
- 如果被重写的方法是 public, 那么重写的方法就只能是 public。
举个例子,父类中的方法是 protected:
public class Animal {
protected void eat() { }
}
子类中的方法可以是 public:
public class Dog extends Animal {
public void eat() { }
}
如果子类中的方法用了更严格的权限修饰符,编译器就报错了。
10.6 规则六:重写后的方法不能抛出比父类中更高级别的异常。
举例来说,如果父类中的方法抛出的是 IOException,那么子类中重写的方法不能抛出 Exception,可以是 IOException 的子类或者不抛出任何异常。这条规则只适用于可检查的异常。
可检查(checked)异常必须在源代码中显式地进行捕获处理,不检查(unchecked)异常就是所谓的运行时异常,比如说 NullPointerException、ArrayIndexOutOfBoundsException 之类的,不会在编译器强制要求。
父类抛出 IOException:
public class Animal {
protected void eat() throws IOException { }
}
子类抛出 FileNotFoundException 是可以满足重写的规则的,因为 FileNotFoundException 是 IOException 的子类。
public class Dog extends Animal {
public void eat() throws FileNotFoundException { }
}
如果子类抛出了一个新的异常,并且是一个 checked 异常:
public class Dog extends Animal {
public void eat() throws FileNotFoundException, InterruptedException { }
}
那编译器就会提示错误:
Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat()
被覆盖的方法未抛出java.lang.InterruptedException
10.7 可以在子类中通过 super 关键字来调用父类中被重写的方法。
public class Animal {
protected void eat() { }
}
子类重写了 eat()
方法,然后在子类的 eat()
方法中,可以在方法体的第一行通过 super.eat()
调用父类的方法,然后再增加属于自己的代码。
public class Dog extends Animal {
public void eat() {
super.eat();
// Dog-eat
}
}
10.8 规则八:构造方法不能被重写。
因为构造方法很特殊,而且子类的构造方法不能和父类的构造方法同名(类名不同),所以构造方法和重写之间没有任何关系。
11.包
Java 定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
。
包可以是多层结构,用.
隔开。例如:java.util
。
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
package_sample
└─ src
├─ hong
│ └─ Person.java
│ ming
│ └─ Person.java
└─ mr
└─ jun
└─ Arrays.java
编译后的.class
文件也需要按照包结构存放。如果使用 IDE,把编译后的.class
文件放到bin
目录下,那么,编译的文件结构就是:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
导入包,在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类,他有三种写法:
第一种,直接写出完整类名,例如:
// Person.java
package ming;
public class Person {
public void run() {
mr.jun.Arrays arrays = new mr.jun.Arrays();
}
}
第二种写法是用import语句 ,例如:
// Person.java
package ming;
// 导入完整类名:
import mr.jun.Arrays;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
还有一种import static
的语法,它可以导入一个类的静态字段和静态方法:
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
12.工厂模式的实现
工厂模式将对象的创建过程封装在工厂类中,客户端代码不需要知道如何实例化对象的细节,客户端只需要请求工厂来获取对象即可。这意味着,工厂类负责对象的创建,客户端只关心如1何使用对象。
class Fruit {
String name;
// 构造方法
public Fruit(String name) {
this.name = name;
}
void grup() {
System.out.println("水果野蛮生长");
}
}
class Apple extends Fruit {
// 重写 grup 方法
@Override
void grup() {
System.out.println("苹果野蛮生长");
}
// 构造方法
public Apple(String name) {
super(name); // 调用父类的构造方法
}
}
class Xigua extends Fruit {
// 重写 grup 方法
@Override
void grup() {
System.out.println("西瓜野蛮生长");
}
// 构造方法
public Xigua(String name) {
super(name); // 调用父类的构造方法
}
}
class Factory {
public Fruit getFruit(String name) {
if (name.equals("苹果")) {
return new Apple("苹果16");
} else if (name.equals("西瓜")) {
return new Xigua("西瓜16");
} else {
return null;
}
}
}
public class Test {
public static void main(String[] args) {
Factory factory = new Factory();
// 使用字符串来调用 getFruit 方法
factory.getFruit("苹果").grup();// 输出:苹果野蛮生长
factory.getFruit("西瓜").grup();// 输出:西瓜野蛮生长
}
}
13.抽象类
抽象类的特点:
1.定义中含有抽象方法的类叫抽象类
2.抽象类用abstract来修饰
3.抽象类代表一种抽象的对象类型
4.抽象类不能实例化
5.抽象类中可以有具体方法,可以没有抽象方法
定义抽象类的时候需要用到关键字 abstract
,放在 class
关键字前,就像下面这样。
abstract class AbstractPlayer {
}
抽象类是不能实例化的,尝试通过 new
关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。
虽然抽象类不能实例化,但可以有子类。子类通过 extends
关键字来继承抽象类。就像下面这样。
public class BasketballPlayer extends AbstractPlayer {
}
如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。
抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样:
public abstract class AbstractPlayer {
abstract void play();
public void sleep() {
System.out.println("运动员也要休息而不是挑战极限");
}
}
抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类 AbstractPlayer 中定义了 play()
方法,子类 BasketballPlayer 中就必须实现。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是张伯伦,篮球场上得过 100 分");
}
}
如果没有实现的话,编译器会提示“子类必须实现抽象方法”,见下图。
13.1 抽象类应用模板方法模式
这个方法调用了抽象方法 initUart()
、getcomand()
、opencurse()
、openlight()
和 openTV()
。但实际上,你并不需要在抽象类中提供这些方法的实现,抽象类的作用只是定义接口,而这些方法的具体实现由子类来完成。
abstract class DD {
// 抽象方法
abstract void initUart();
abstract void getcomand();
abstract void opencurse();
abstract void openlight();
abstract void openTV();
// 抽象类中的工作方法
void work() {
initUart();
getcomand();
opencurse();
openlight();
openTV();
}
}
class STC15wCon extends DD {
@Override
void initUart() {
System.out.println("STC15wCon initUart");
}
@Override
void getcomand() {
System.out.println("STC15wCon getcomand");
}
@Override
void opencurse() {
System.out.println("STC15wCon opencurse");
}
@Override
void openlight() {
System.out.println("STC15wCon openlight");
}
@Override
void openTV() {
System.out.println("STC15wCon openTV");
}
}
public class Test {
public static void main(String[] args) {
STC15wCon c = new STC15wCon(); // 注意这里需要括号
c.work(); // 调用work方法
}
}
输出结果:
这样,
work()
方法在DD
抽象类中被定义,并且通过子类STC15wCon
提供了各个具体方法的实现。当c.work()
被调用时,所有的抽象方法都被执行。STC15wCon initUart STC15wCon getcomand STC15wCon opencurse STC15wCon openlight STC15wCon openTV
14.接口
接口通过 interface 关键字来定义,它可以包含一些常量和抽象方法。
public interface Electronic {
// 常量
String LED = "LED";
// 抽象方法
int getElectricityUse();
// 静态方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}
// 默认方法
default void printDescription() {
System.out.println("电子");
}
}
1)接口不允许直接实例化,否则编译器会报错。
public class Computer implements Electronic {
public static void main(String[] args) {
new Computer();
}
@Override
public int getElectricityUse() {
return 0;
}
}
2)接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 Java.io包下。
public interface Serializable {
}
Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。
4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。
在接口中,所有的 方法默认就是抽象的,因此你不需要显式地使用 abstract
来声明它们。接口中的方法默认是 public abstract
,即使不写 abstract
,编译器会隐式地将其视为抽象方法。
5)接口的变量是隐式 public static final
(常量),所以其值无法改变。
15.object类
在 Java 中,所有类都隐式地继承自 Object
类,除非显式声明继承自其他类。Object
是 Java 类层次结构中的根类,它提供了许多重要的方法,所有继承自 Object
的类都能使用这些方法。因为所有类都继承自 Object
,你几乎总是会用到 Object
类的一些方法。
1.toString()
:
- 该方法用于返回对象的字符串表示。默认实现返回的是对象的类名和哈希码,但通常我们会重写它以提供更有意义的输出。
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person("Alice", 25);
System.out.println(p.toString()); // 输出:Person[name=Alice, age=25]
}
}
2.equals(Object obj)
:
equals()
用于比较两个对象是否相等。默认实现比较的是对象的引用是否相同(即它们是否指向相同的内存地址)。为了使其按内容比较(比如比较字符串内容或对象的属性),通常需要重写该方法。
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // 输出:true
}
}
3.hashCode()
:
hashCode()
方法用于返回对象的哈希码,通常与equals()
方法一起使用。两者应该遵循一个原则:如果两个对象通过equals()
比较相等,它们的hashCode()
必须相同。- 默认实现基于对象的内存地址,但通常我们会根据对象的属性来重写
hashCode()
方法。
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4.clone()
:
clone()
方法用于创建当前对象的副本。为了能使用此方法,类需要实现Cloneable
接口,否则会抛出CloneNotSupportedException
异常。
class Person implements Cloneable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("Alice", 25);
Person p2 = (Person) p1.clone();
System.out.println(p1 == p2); // 输出:false,说明是不同的对象
}
}
5.getClass()
:
getClass()
返回当前对象的Class
实例,代表该对象的运行时类。这对于反射操作非常有用。
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person("Alice", 25);
System.out.println(p.getClass().getName()); // 输出:Person
}
}
6.具体综合实例
- 在 Java 中,所有类都继承自
Object
类,即使没有显式声明继承,也会继承Object
类的方法。Object
类提供了很多通用的方法,如toString()
、equals()
、hashCode()
、getClass()
、clone()
等,通常会根据实际需求重写这些方法。- 重写
equals()
和hashCode()
是非常常见的做法,尤其是在处理集合类时,比如使用HashMap
、HashSet
等时,Java 会依赖这些方法来判断对象是否相等或进行对象存储。
class Person {
String name;
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person[name=" + name + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;//强制转换为Person对象
return name.equals(person.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
System.out.println(p1.toString()); // 输出:Person[name=Alice]
System.out.println(p1.equals(p2)); // 输出:true
System.out.println(p1.hashCode()); // 输出:哈希码
}
}
16.内部类
(1) 成员内部类
成员内部类:创建成员内部类对象时需要先创建外部类的对象,因为成员内部类实例是与外部类实例相关联的。
成员内部类是最常见的内部类,看下面的代码:
class Wanger {
int age = 18;
class Wangxiaoer {
int age = 81;
}
}
成员内部类可以无限制访问外部类的所有成员属性。
public class Wanger {
int age = 18;
private String name = "沉默王二";
static double money = 1;
class Wangxiaoer {
int age = 81;
public void print() {
System.out.println(name);
System.out.println(money);
}
}
}
内部类可以随心所欲地访问外部类的成员,但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问:
public class Wanger {
int age = 18;
private String name = "沉默王二";
static double money = 1;
public Wanger () {
new Wangxiaoer().print();
}
class Wangxiaoer {
int age = 81;
public void print() {
System.out.println(name);
System.out.println(money);
}
}
}
如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的。
public class Wanger {
int age = 18;
private String name = "沉默王二";
static double money = 1;
public Wanger () {
new Wangxiaoer().print();
}
public static void main(String[] args) {
Wanger wanger = new Wanger();
Wangxiaoer xiaoer = wanger.new Wangxiaoer();
xiaoer.print();
}
class Wangxiaoer {
int age = 81;
public void print() {
System.out.println(name);
System.out.println(money);
}
}
}
外部类名.内部类名 实例 = 外部类实例名.new 内部类构造方法(参数)
(2) 局部内部类
局部内部类:局部内部类通常在外部方法中创建实例,不需要创建外部类的实例(如果局部内部类是该方法中的一部分)。
public class OuterClass {
public void outerMethod() {
int num = 10; // 方法中的局部变量
final int factor = 2; // final 局部变量
// 局部内部类
class InnerClass {
public void display() {
System.out.println("num: " + num); // 可以访问 num
System.out.println("factor: " + factor); // 可以访问 final 局部变量
}
}
// 创建局部内部类的实例
InnerClass inner = new InnerClass();
inner.display();
}
}
public class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.outerMethod(); // 调用外部类方法,进而访问局部内部类
}
}
注意事项:
- 局部内部类只能在其所在的方法、构造函数或代码块中使用,不能在方法外部声明或使用。
- 由于它们访问外部方法中的局部变量,这些变量必须是
final
或有效final
,即不能被重新赋值。
(3) 匿名内部类
匿名内部类是唯一一种没有构造方法的类。就上面的写法来说,匿名内部类也不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象。
匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
}
}
(4) 静态内部类
静态内部类和成员内部类类似,只是多了一个 static关键字。
由于 static 关键字的存在,静态内部类是不允许访问外部类中非 static 的变量和方法的。
public class Wangsi {
static int age;
double money;
static class Wangxxiaosi {
public Wangxxiaosi (){
System.out.println(age);
}
}
}
17.多态
多态存在的三个必要条件:
(1)需要存在继承和实现关系
(2)同样的方法调用而执行不同操作,运行不同代码(重写)
(3)在运行时父类或者接口的引用变量可以引用其子类的对象多态的对象
abstract class Animal {
public String name;
// 抽象的 eat 方法,没有参数
abstract public void eat();
}
class Dog extends Animal {
@Override
public void eat() {
// 在这里使用类的 name 属性,假设 name 已经被赋值
System.out.println(name + " is eating.");
}
// 如果想要使用一个方法设置 name,可以新定义一个方法
public void setName(String name) {
this.name = name;
}
}
class cat extends Animal {
@Override
public void eat() {
// 在这里使用类的 name 属性,假设 name 已经被赋值
System.out.println(name + " is eating.");
}
// 如果想要使用一个方法设置 name,可以新定义一个方法
public void setName(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
animal an1 = new Dog();
animal an2 = new cat();
an1.eat();
an2.eat();
}
}
18.多态中上下转型的关系--instanceof关键字
(object) instanceof (type)
用意也非常简单,判断对象是否符合指定的类型,结果要么是 true,要么是 false。在反序列化的时候,instanceof 操作符还是蛮常用的,因为这时候我们不太确定对象属不属于指定的类型,如果不进行判断的话,就容易抛出 ClassCastException 异常。
(1)新建一个接口 Shape:
interface Shape {
}
(2)然后新建 Circle 类实现 Shape 接口并继承 Round 类:
class Circle extends Round implements Shape {
}
(3)如果对象是由该类创建的,那么 instanceof 的结果肯定为 true。
Circle circle = new Circle();
System.out.println(circle instanceof Circle);
(4)这个肯定没毛病,instanceof 就是干这个活的,大家也很好理解。那如果类型是父类呢?
System.out.println(circle instanceof Round);
(5)结果肯定还是 true,因为依然符合 is-a
的关系。那如果类型为接口呢?
System.out.println(circle instanceof Shape);
(6)结果仍然为 true, 因为也符合 is-a
的关系。如果要比较的对象和要比较的类型之间没有关系,当然是不能使用 instanceof 进行比较的。
为了验证这一点,我们来创建一个实现了 Shape 但与 Circle 无关的 Triangle 类:
class Triangle implements Shape {
}
这时候,再使用 instanceof 进行比较的话,编译器就报错了。
System.out.println(circle instanceof Triangle);
错误信息如下所示:
Inconvertible types; cannot cast 'com.itwanger.twentyfour.instanceof1.Circle' to 'com.itwanger.twentyfour.instanceof1.Triangle'
意思就是类型不匹配,不能转换,我们使用 instanceof 比较的目的,也就是希望如果结果为 true 的时候能进行类型转换。
(7)强转类型
先用 instanceof 进行类型判断,然后再把 obj 强制转换成我们期望的类型再进行使用。
// 先判断类型
if (obj instanceof String) {
// 然后强制转换
String s = (String) obj;
// 然后才能使用
}
JDK 16 的时候,instanceof 模式匹配转了正,意味着使用 instanceof 的时候更便捷了。
if (obj instanceof String s) {
// 如果类型匹配 直接使用 s
}
19.异常
“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。”
19.1 checked和unchecked异常
checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出
unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。
首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。
建议你要么使用 try-catch 进行捕获:
try {
Class clz = Class.forName("com.itwanger.s41.Demo1");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
注意打印异常堆栈信息的 printStackTrace()
方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。
要么在方法签名上使用 throws 关键字抛出:
public class Demo1 {
public static void main(String[] args) throws ClassNotFoundException {
Class clz = Class.forName("com.itwanger.s41.Demo1");
}
}
19.2 NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。
- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。
19.3 try-catch-finally
(1)try{ }语句块中放的是要检测的java代码,可能有会抛出异常,也可能会正常执行。
(2)catch(异常类型){ }块是当java运行时系统接收到try块中所抛出异常对象中,会寻找能处理这一异常cathc块来进行处理(可以有多个cathc块)。
(3)finally{ }不管系统有没有抛出异常都会去执行,一般用来释放资源。除了在之前执行了System.exit(0)
19.4 throw和throws
throw:用于手动抛出异常,作为程序员可以在任意位置手动抛出异常。
throws:用于在方法上标识要暴露的异常。抛出的异常交由调用者处理。
public class ThrowDemo {
static void checkEligibilty(int stuage){
if(stuage<18) {
throw new ArithmeticException("年纪未满 18 岁,禁止观影");
} else {
System.out.println("请认真观影!!");
}
}
public static void main(String args[]){
checkEligibilty(10);
System.out.println("愉快地周末..");
}
}
这段代码在运行的时候就会抛出以下错误:
Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影 at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9) at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16)
20.泛型类
class ClassName<T> {
private T variable; // T 是泛型参数,可以是任意类型
public ClassName(T variable) {
this.variable = variable;
}
public T getVariable() {
return variable;
}
public void setVariable(T variable) {
this.variable = variable;
}
}
在上面的代码中,T
是一个类型参数,它表示在使用 ClassName
时可以传入任何类型。
使用泛型类:
你可以在创建 ClassName
类的对象时,指定具体的类型:
public class Main {
public static void main(String[] args) {
// 创建泛型类的实例,类型为 String
ClassName<String> stringInstance = new ClassName<>("Hello, Generics!");
System.out.println(stringInstance.getVariable()); // 输出: Hello, Generics!
// 创建泛型类的实例,类型为 Integer
ClassName<Integer> integerInstance = new ClassName<>(100);
System.out.println(integerInstance.getVariable()); // 输出: 100
}
}
20.1 泛型类的多个类型参数
你可以在一个泛型类中使用多个类型参数,这样它就能够处理多个不同的类型。
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
// 创建一个键值对,键是 String,值是 Integer
Pair<String, Integer> pair = new Pair<>("Age", 25);
System.out.println(pair.getKey() + ": " + pair.getValue()); // 输出: Age: 25
}
}
20.2 通配符(Wildcard)
泛型类的一个强大特性是你可以使用通配符来表示不关心的类型。例如,使用 ?
表示任意类型。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
printList(stringList); // 输出:Hello World
printList(integerList); // 输出:1 2
}
}
list
是你传入的List<?>
,其中?
是通配符,表示这个list
可以包含任何类型的对象。for (Object obj : list)
的意思是:遍历list
中的每个元素,将每个元素依次赋值给变量obj
,然后执行循环体中的代码。由于
list
的类型是List<?>
(一个泛型List
,但是具体的类型我们并不关心),obj
的类型是Object
。在 Java 中,所有类都是Object
类的子类,因此Object
可以接收list
中的任何元素类型。
new ArrayList<>()
是在创建一个新的ArrayList
实例,表示这是一个存储元素的可变大小的列表。- 尽管你没有显式地指定泛型类型,编译器能够根据你赋值给
List<String>
或List<Integer>
来推断出ArrayList<String>
或ArrayList<Integer>
。
20.3 泛型继承
class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
class ColoredBox<T> extends Box<T> {
private String color;
public ColoredBox(T value, String color) {
super(value); // 调用父类的构造函数
this.color = color;
}
public String getColor() {
return color;
}
}
使用泛型类的继承
public class Main {
public static void main(String[] args) {
ColoredBox<String> coloredBox = new ColoredBox<>("Hello", "Red");
System.out.println(coloredBox.getValue()); // 输出: Hello
System.out.println(coloredBox.getColor()); // 输出: Red
}
}
20.4 泛型类型可以实现泛型接口
interface PairInterface<K, V> {
K getKey();
V getValue();
}
class PairClass<K, V> implements PairInterface<K, V> {
private K key;
private V value;
PairClass(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
PairClass<String, Integer> pair = new PairClass<>("One", 1);
System.out.println(pair.getKey() + ": " + pair.getValue()); // 输出: One: 1
}
}
- 接口本身是泛型的:你可以定义一个泛型接口,接口中可以有类型参数。
- 实现接口时,可以传递具体的类型:实现泛型接口的类也可以是泛型类,或者你可以指定具体的类型来实现接口。
- 灵活性:泛型接口使得你能够定义一些与类型无关的行为,然后由实现该接口的类来具体化实现时的类型。
20.5 继承多个泛型参数及实现泛型接口
// 泛型接口,接受一个泛型参数
interface Printable<T> {
void print(T item);
}
// 泛型类,接受两个泛型参数
class Storage<T, U> {
private T item; // 存储的物品
private U metadata; // 与物品相关的元数据
public Storage(T item, U metadata) {
this.item = item;
this.metadata = metadata;
}
public T getItem() {
return item;
}
public U getMetadata() {
return metadata;
}
public void setItem(T item) {
this.item = item;
}
public void setMetadata(U metadata) {
this.metadata = metadata;
}
}
// 继承 Storage 并实现 Printable 的泛型类
class AdvancedStorage<T, U> extends Storage<T, U> implements Printable<T> {
public AdvancedStorage(T item, U metadata) {
super(item, metadata);
}
@Override
public void print(T item) {
System.out.println("Printing item: " + item);
}
// 自定义方法,显示存储的物品和元数据
public void showItemAndMetadata() {
System.out.println("Stored item: " + getItem() + ", Metadata: " + getMetadata());
}
}
public class Main {
public static void main(String[] args) {
// 创建一个 AdvancedStorage 实例,存储 String 类型的数据和 Integer 类型的元数据
AdvancedStorage<String, Integer> stringStorage = new AdvancedStorage<>("Hello, World!", 2025);
stringStorage.showItemAndMetadata(); // 输出:Stored item: Hello, World!, Metadata: 2025
stringStorage.print(stringStorage.getItem()); // 输出:Printing item: Hello, World!
// 创建一个 AdvancedStorage 实例,存储 Integer 类型的数据和 Double 类型的元数据
AdvancedStorage<Integer, Double> integerStorage = new AdvancedStorage<>(123, 45.67);
integerStorage.showItemAndMetadata(); // 输出:Stored item: 123, Metadata: 45.67
integerStorage.print(integerStorage.getItem()); // 输出:Printing item: 123
}
}
Main
类:在main()
方法中,我们创建了AdvancedStorage
的两个实例,一个存储String
类型的数据和Integer
类型的元数据,另一个存储Integer
类型的数据和Double
类型的元数据。然后,我们调用了showItemAndMetadata()
和print()
方法来演示如何处理和打印这些数据。
21.限制泛型类型
- 限制为某个类的子类:使用
extends
来限制泛型类型为某个类或其子类。 - 限制为实现某个接口:使用
extends
来限制泛型类型为实现某个接口的类型。
21.1 限制泛型类型为某个类的子类或实现接口
// 限制泛型类型为 Number 或其子类
class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void printValue() {
System.out.println("Value: " + value);
}
}
public class Main {
public static void main(String[] args) {
NumberBox<Integer> integerBox = new NumberBox<>(10); // Integer 是 Number 的子类
integerBox.printValue(); // 输出:Value: 10
NumberBox<Double> doubleBox = new NumberBox<>(3.14); // Double 是 Number 的子类
doubleBox.printValue(); // 输出:Value: 3.14
// 以下代码会报错,因为 String 不是 Number 的子类
// NumberBox<String> stringBox = new NumberBox<>("Hello");
}
}
NumberBox<T extends Number>:T 必须是 Number 或其子类(如 Integer, Double 等)。你无法将 String 类型作为 T 传递给 NumberBox,因为 String 不是 Number 或其子类。
21.2 限制泛型为多个接口的实现
// 定义两个接口
interface Printable {
void print();
}
interface Showable {
void show();
}
// 泛型类,限制 T 必须实现 Printable 和 Showable 接口
class Display<T extends Printable & Showable> {
private T item;
public Display(T item) {
this.item = item;
}
public void displayItem() {
item.print(); // 调用 print 方法
item.show(); // 调用 show 方法
}
}
// 实现这两个接口的类
class Product implements Printable, Showable {
private String name;
public Product(String name) {
this.name = name;
}
@Override
public void print() {
System.out.println("Product name: " + name);
}
@Override
public void show() {
System.out.println("Showing product: " + name);
}
}
public class Main {
public static void main(String[] args) {
Product product = new Product("Laptop");
Display<Product> display = new Display<>(product);
display.displayItem();
}
}
T extends Printable & Showable:T 必须同时实现 Printable 和 Showable 接口。在 Display<T> 中,T 必须有 print 和 show 方法。
21.3 使用通配符(Wildcard)进行限制
// 使用上界通配符
public class UpperBoundExample {
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
integers.add(10);
integers.add(20);
List<Double> doubles = new ArrayList<>();
doubles.add(3.14);
doubles.add(2.71);
printNumbers(integers); // 输出:10 20
printNumbers(doubles); // 输出:3.14 2.71
}
}
通配符,上下转型中的应用。
class animal{
}
class dog extends animal{
}
class Cls1<T>{
T a;
public Cls1(T a){
this.a = a;
}
public T getData(){
return a;
}
}
public class test{
public static void main(String[] args){
Cls1<? extends animal>c2;
Cls1<? super dog>c3;
Cls1<dog> c1=new Cls1<dog>(new dog());
c2=c1;
c3=c1;
}
}
21.4 泛型方法
访问修饰符 <泛型列表> 返回类型 方法名(参数列表){
实现代码
}
21.4.1 多泛型参数的泛型方法
public class MultiGenericMethod {
// 定义一个多泛型参数的方法
public static <T, U> void printPair(T first, U second) {
System.out.println("First: " + first);
System.out.println("Second: " + second);
}
public static void main(String[] args) {
printPair(10, "Hello"); // 输出:First: 10, Second: Hello
printPair(3.14, true); // 输出:First: 3.14, Second: true
}
}
21.4.2 通配符和泛型方法结合使用
public class WildcardGenericMethod {
// 使用通配符上界来限制泛型类型
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> integers = List.of(1, 2, 3);
List<Double> doubles = List.of(3.14, 2.71);
printNumbers(integers); // 输出:1 2 3
printNumbers(doubles); // 输出:3.14 2.71
}
}
printNumbers(List<? extends Number> list):
- 这个方法接受一个 List,其元素类型是 Number 或其子类。
?extends Number 表示 List 中的元素类型可以是 Number 或 Number 的任何子类(如 Integer, Double)。
- 通过通配符,方法可以接收多种类型的 List。
二.Android项目正式开始
1.双击运行
2.选择路径
3.结束
4.准备运行环境
如果你电脑已有jdk环境,高于11以上即可,若版本太低可能会导致AndroidStudio一直初始化失败,建议下我所提供的jdk版本,电脑可以共存多个jdk,如若已经其他jdk配置环境变量,即可跳过此步骤
(1)环境配置
个人建议将jdk17工具包解压放在C盘根目录下,如下图
(2)环境变量配置
若你跟我一样默认放在C盘,那么位置就是C:\jdk-17.0.0.1
接下来开始配置,找到系统环境设置(win11若你经常配置环境变量,应该是知道在哪里配置,设置→系统→系统信息→高级系统设置→高级→环境变量),不会的请自己百度
主要是配置下面的系统变量
你先把这两句复制到剪切板
JAVA_HOME
%JAVA_HOME%\bin
开始配置:
点击确定保存,然后往下找到变量为Path的,双击打开
点击新建,将%JAVA_HOME%\bin填入,然后将其移动到最上方即可,防止其他环境冲突
记住一定要点击确认,全部确认,此时你就已经完成了jdk17的环境配置,但是需要检验是否配置成功,在cmd命令窗口中输入
java --version
若不是如下图,那你就只有自己检查是否哪里配置错误,当然你配置的其他版本jdk,那么版本肯定是不同的,我所演示是jdk17
(3)SDK环境安装配置
个人建议单独放在其他盘,专门建立个文件夹存放sdk 现在双击打开AndroidStudio运行,有些可能桌面安装没有快捷方式,你只需要按win键,找到所有英语,A字母开头的地方就能找到(以下操作请保持网络环境较好情况,防止出错)
1.打开记得点击don’t send
2.第一次会碰见以下,直接先取消
3. 然后点击Next
,然后记得把这个sdk更换到其他盘,我就放到了E盘,建立了个文件夹
4. 然后点击Next,出现以下界面继续NextAccept,在点击AcceptF,然后Finish,等待下载即可
5.直到出现以下Finish的方框变蓝可以点击的时候证明完成了,点击即可
(4)关键的地方
到这个地方先叉掉软件,我们先继续配置环境,同样先将以下代码复制保存在剪切板中
ANDROID_HOME
%ANDROID_HOME%\tools
%ANDROID_HOME%\platform-tools
打开最初的环境变量配置设置(和配置JDK17一样的办法),点击环境变量新建,如下图
点击确认保存,再点击Path,添加以下两个
%ANDROID_HOME%\tools
%ANDROID_HOME%\platform-tools
此时就完成了SDK的配置
(5)Gradle依赖库配置
首先你得先建立一个名为GradleRepository
的文件夹(自定义目录),一 会儿进行配置,以下截图是我的路径,仅作为参考.复制以下文本,一会儿环境配置所用👇
GRADLE_USER_HOME
一样打开你的环境变量配置,按照之前操作,新建环境变量
点击确定(不点击确定可能会保存失败,两次确认关闭),然后!强烈建议重启你的电脑,强烈建议!!!
5.运行软件,进行最后的配置
1.检查相应设置是否调整准确
然后点击OK
2.进行Gradle下载和GradleRepository依赖库下载
直到出现以下界面
以下操作个人强烈建议先断网,不然要操作几次,才能配置成功,也可以不断网,但保守嘛
将下载源更改为腾讯镜像源
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
切记,只需要把services.gradle.org/distributions替换成mirrors.cloud.tencent.com/gradle就行了,其余地方不要动!!
然后在找默认依赖库下载源,只需要第一次更改,之后就不需要了,只下载一次,如若不更改,可能会下载很慢,长达一个小时,如若更改为国内源,几分钟的事情 找到settings.gradle.kts
将上图都替换成以下代码(这是2025年2月6日最新更新的源)
pluginManagement {
repositories {
maven { setUrl("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/") }
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/central")}
maven { setUrl("https://maven.aliyun.com/nexus/content/groups/public/")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/google")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/jcenter")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/gradle-plugin")}
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { setUrl("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/") }
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/central")}
maven { setUrl("https://maven.aliyun.com/nexus/content/groups/public/")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/google")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/jcenter")}
maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/gradle-plugin")}
google()
mavenCentral()
}
}
rootProject.name = "TestApplication"
include(":app")
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
最好是Ctrl+S一下下吧,保守以下 此时你点击左上角的那个Try Again
,记得联网,等待下载完成
三.Android如何导入其他项目
一般下载下来的项目都有这些文件夹,在导入项目之前,首先删除gradle文件夹和下面红框的几个文件夹,其次打开整个项目的build.gradle文件,可以直接用记事本打开,就是下图中红圈里面的
打开之后找到下图中的这一块,然后修改后面的版本号,我图里面的是2.2.2,自己可以看一下你平时项目的这个文件使用的版本号,然后进行修改。
修改完成之后,才可开始导入,但是导入的时候会弹出一个窗口。
是否使用gradle wrapper,必须取消,就是每次导入其他人的项目特别慢的根源所在
取消之后会让你手动选择gradle版本,一般是在Android studio 的安装目录的gradle文件夹下面
例如:C:\Program Files\android\Android Studio\gradle
这个文件夹下面有当前可用的gradle版本,选择一个类似gradle-xxx的文件夹。然后确认,导入即可完成。