面向对象进阶
static
静态变量
-
被该类的所有的对象共享
-
不属于对象,属于类
-
随着类的加载而加载,优先于对象存在
-
类名调用(推荐)
-
对象名调用
static的注意事项
-
静态方法中没有this关键字
-
静态方法,只能访问静态
-
非静态方法可以访问所有
在Java中,static
关键字用于管理类的静态成员。静态变量(也称为静态字段或类变量)是类的成员变量,它不属于类的任何特定实例,而是属于类本身。这意味着类的所有实例共享同一个静态变量。当某个实例修改了静态变量的值时,这个修改对所有其他实例都是可见的。
特性
- 类级别的变量:静态变量属于整个类,而不是类的任何特定实例。
- 内存管理:静态变量在程序开始时创建,在程序结束时销毁,它们不像实例变量那样随着对象的创建而创建,随着对象的销毁而销毁。
- 默认值:静态变量会被自动初始化为其数据类型的默认值(例如,
int
的默认值为0,对象引用的默认值为null
)。 - 全局访问:静态变量可以通过类名直接访问,不需要创建类的实例。
使用场景
- 常量:静态变量常用于定义常量。结合
final
关键字使用,可以创建类级别的常量。 - 计数器:静态变量可以用作计数器来记录类的实例数量或跟踪其他类级别的信息。
- 工具类:在不需要实例化对象的工具类中,所有方法和变量通常都是静态的。
- 配置信息:静态变量可以存储配置信息,如数据库连接、应用设置等。
示例
下面是一个使用静态变量的简单示例:
public class Counter {
// 静态变量,用于跟踪Counter类的实例数量
public static int count = 0;
public Counter() {
count++; // 每次创建Counter实例时,count都会递增
}
}
public class TestCounter {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
// 所有实例共享同一个静态变量count
System.out.println("Number of instances: " + Counter.count); // 输出: Number of instances: 3
}
}
注意事项
- 内存泄漏:静态变量如果不当使用,可能会导致内存泄漏。例如,如果它们引用了一些不再使用的对象,这些对象就不会被垃圾回收器回收,因为静态变量的生命周期很长。
- 线程安全:静态变量在多线程环境中共享,因此必须确保对静态变量的访问是线程安全的。
- 设计考虑:过度使用静态变量可能导致面向对象设计的问题,如难以测试、难以维护和扩展等。
在Java中,static
关键字用于创建类级别的变量和方法,而不是实例级别的。使用static
时,需要注意以下几点:
-
加载时机:
static
变量和方法在类被加载到JVM时就已经被初始化和分配内存空间,而不是在创建对象实例时。因此,static
变量和方法可以直接通过类名来访问,无需创建对象。 -
共享性:所有对象实例共享同一份
static
变量的副本。因此,对static
变量的修改会影响到所有对象实例。 -
访问限制:
<mark><mark><mark><mark><mark><mark>static
方法不能直接访问非static
变量和方法,因为非static
变量和方法需要依赖具体的对象实例。如果需要访问,需要通过对象实例来调用。 -
初始化顺序:
static
变量的初始化顺序按照声明的顺序进行,而不是按照赋值语句的顺序。静态初始化块(static initializer block)也会按照在代码中出现的顺序执行。 -
线程安全:如果多个线程同时访问和修改
static
变量,可能会导致数据不一致的问题。因此,在多线程环境下使用static
变量时,需要注意线程安全问题,可以考虑使用synchronized
关键字或其他并发控制机制。 -
内存管理:由于
static
变量在类加载时就已经分配内存空间,因此其生命周期与整个程序的运行时间相同。如果static
变量占用大量内存或持有对不再需要的对象的引用,可能会导致内存泄漏问题。因此,在使用static
变量时,需要注意及时释放不再需要的资源。 -
设计原则:尽量避免过度使用
static
,因为它可能会导致代码难以维护和测试。过度依赖static
可能会使代码变得紧耦合,降低代码的可重用性和可扩展性。在设计类时,应该尽量遵循面向对象的设计原则,如封装、继承和多态等。
总之,在使用Java中的static
关键字时,需要谨慎考虑其影响和作用范围,确保代码的正确性和可维护性。
总结来说,静态变量是Java中一个非常有用的特性,但需要谨慎使用,以避免潜在的问题。
工具类
在Java中,工具类(Utility Class)是一种常用的设计模式,它提供了一系列静态方法以执行常见的任务,而不需要创建该类的实例。这些静态方法通常执行一些简单但频繁需要的操作,例如字符串操作、日期处理、文件读写等。
以下是一些常见的Java工具类及其功能:
- StringUtils: 用于处理字符串的常用操作,如判空、拼接、替换、大小写转换等。Apache Commons Lang库提供了这样一个类。
- DateUtils: 用于处理日期和时间的工具类,如格式化日期、解析日期字符串、计算日期差等。Apache Commons Lang和Java 8的java.time包都提供了这样的功能。
- FileUtils: 用于文件的读写、复制、移动、删除等操作。Apache Commons IO库提供了这样一个类。
- NumberUtils: 用于处理数字的工具类,如将字符串转换为数字、比较数字等。Apache Commons Lang库提供了这样的功能。
- ArrayUtils: 用于处理数组的工具类,如判断数组是否为空、合并数组、反转数组等。Apache Commons Lang库提供了这样的功能。
- CollectionUtils: 用于处理集合的工具类,如判断集合是否为空、集合的交集、并集等操作。Apache Commons Collections库提供了这样的功能。
此外,还有许多其他的工具类,这些类通常根据特定的需求或库而设计。
在设计工具类时,通常遵循以下原则:
- 工具类应该是不可实例化的,通常通过将构造器设为私有来实现。
- 工具类中的方法应该是静态的,这样可以直接通过类名调用,而不需要创建对象。
- 工具类通常不会持有任何状态(即没有实例变量),因为它们的目的只是执行一些简单的操作。
例如,一个简单的字符串工具类可能如下所示:
public final class StringUtils { private StringUtils() { // 私有构造器,防止实例化 } public static boolean isNotEmpty(String str) { return str != null && !str.isEmpty(); } public static String join(String delimiter, String... elements) { if (elements == null || elements.length == 0) { return ""; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < elements.length; i++) { if (i > 0) { sb.append(delimiter); } sb.append(elements[i]); } return sb.toString(); } // 其他字符串操作方法... }
这个工具类提供了两个静态方法:isNotEmpty
用于检查字符串是否非空,join
用于将多个字符串使用指定的分隔符连接起来。由于构造器是私有的,因此这个类不能被实例化。
继承
在Java中,继承(Inheritance)是面向对象编程(OOP)的四大基本特性之一,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以自动获得父类的所有非私有属性和方法,并且可以添加新的属性和方法,或者重写父类中的方法以实现特定的行为。
继承的主要优点包括:
- 代码重用:子类可以重用父类的代码,避免了重复编写相同的代码。
- 扩展性:子类可以在父类的基础上添加新的功能,从而扩展父类的功能。
- 多态性:继承是实现多态性的基础,多态性允许使用父类类型的引用来引用子类对象,并调用子类重写的方法。
Java中的继承是通过使用extends
关键字来实现的。以下是一个简单的继承示例:
// 父类
class Animal {
void eat() {
System.out.println("Animal eats");
}
}
// 子类
class Dog extends Animal {
void bark() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // 调用继承自Animal的eat方法
myDog.bark(); // 调用Dog类特有的bark方法
}
}
在这个例子中,Dog
类继承了Animal
类,因此Dog
类的对象可以调用eat
方法(继承自Animal
类)和bark
方法(Dog
类特有的方法)。
需要注意的是,Java只支持单继承,即一个类只能直接继承自一个父类。但是,一个类可以实现多个接口(Interface),从而具有多个接口的行为。此外,Java中的继承是传递性的,如果一个类继承自另一个类,而那个类又继承自第三个类,那么第一个类将自动继承第三个类的所有属性和方法(除非被中间的类覆盖)。
继承的格式
-
public class 子类 extends 父类{ }
-
核心1: 共性内容抽取
-
核心2: 子类是父类中的一种
-
设计继承图: 从下往上
-
写代码: 从上往下
在Java中,子类可以继承父类的以下内容:
- 实例变量(成员变量):子类可以继承父类中声明的所有非私有(public,protected,默认(无修饰符))的实例变量。如果父类中的实例变量是私有的(private),则子类无法直接访问,但可以通过父类的公共方法(getter和setter方法)来间接访问。
- 方法:子类可以继承父类中声明的所有非私有的方法。子类可以直接调用继承的方法,也可以覆盖(重写)这些方法以提供自己的实现。
- 常量:子类可以继承父类中声明的所有常量(即使用
final
修饰的变量)。这些常量在子类中是可见的,并且不能被修改。
请注意以下几点:
- 子类不能继承父类的构造方法。子类可以调用父类的构造方法,但需要使用
super
关键字来显式调用。 - 子类不能继承父类的私有成员(包括实例变量和方法)。私有成员在父类外部是不可见的,因此子类无法直接访问它们。
- 子类可以访问父类的受保护(protected)成员。受保护的成员在子类中可见,但在其他不相关的类中是不可见的。
- 如果子类覆盖了父类的方法,那么在子类中调用该方法时,将执行子类中的方法实现,而不是父类中的方法实现。
总的来说,子类可以继承父类中的非私有实例变量、方法和常量。子类可以根据需要添加新的成员或覆盖父类的方法,以实现自己的特定功能。
方法的重写:
在Java中,方法的重写(Override)是面向对象编程中的一个重要概念,它允许子类提供一个与父类中方法相同名称、相同参数列表(包括参数类型和顺序)的方法。当子类对象调用这个方法时,将执行子类中的方法,而不是父类中的方法。这有助于实现多态性,使得父类类型的引用可以指向子类对象,并调用子类实现的方法。
重写方法需要满足以下条件:
- 方法名、参数列表必须与父类中被重写的方法相同。
- 访问权限不能低于父类中被重写的方法的访问权限。
- 返回类型必须与父类中被重写的方法的返回类型相同或是其子类型(协变返回类型)。
- 子类方法抛出的异常类型必须与父类中被重写方法的异常类型相同或是其子类型。
- 子类方法的主体实现了父类方法的特定行为。
下面是一个简单的示例,展示了如何在Java中重写方法:
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
// 重写父类的makeSound方法
@Override
void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // 使用父类类型的引用指向子类对象
myDog.makeSound(); // 输出:The dog barks
}
}
在这个示例中,Animal
类有一个makeSound
方法,它输出一条通用消息。Dog
类继承自Animal
类,并重写了makeSound
方法,以输出一条特定于狗的消息。在Main
类的main
方法中,我们使用Animal
类型的引用myDog
指向Dog
对象,并调用makeSound
方法。由于方法重写,实际调用的是Dog
类中的makeSound
方法,输出结果为“The dog barks”。
package com.mohuanan.a006;
public class Person {
String name;
int age;
public Person() {
System.out.println("父类的无参构造");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
package com.mohuanan.a006;
public class Student extends Person{
public Student(){
//会有一个默认的super 先去调用父类里的无参构造 在到自己的本类里去找
System.out.println("子类的无参构造");
}
}
package com.mohuanan.a006;
public class Test {
public static void main(String[] args) {
//创建Student对象
//使用Student里的无参构造
Student s = new Student();
}
}
为什么调用了父类里的构造方法了,还会输出System.out.println("子类的无参构造")???
在Java中,当一个类继承另一个类时,子类的构造方法默认会调用父类的无参构造方法(如果没有明确指定要调用哪个父类构造方法)。这个行为是通过super()
调用实现的,它是子类构造方法中的一个隐含调用。
在你的代码中,Student
类继承了Person
类,并且在Student
类中定义了一个无参构造方法。虽然你没有在Student
的无参构造方法中显式地调用super()
,但是编译器会自动在Student
的无参构造方法开始处插入super();
,这意味着在Student
的构造方法执行之前,会先执行Person
类的无参构造方法。
这是输出你所看到的结果的原因:
- 当你创建
Student
对象时(Student s = new Student();
),首先会执行Student
类的构造方法。 - 在
Student
类的构造方法开始执行之前,由于super();
的隐含调用,会先执行Person
类的无参构造方法。 Person
类的无参构造方法执行时,会输出System.out.println("父类的无参构造");
。- 接着,
Person
类的无参构造方法执行完毕,控制流返回Student
类的构造方法。 - 在
Student
类的构造方法中,输出System.out.println("子类的无参构造");
。
因此,你会看到先输出“父类的无参构造”,然后输出“子类的无参构造”。这是因为即使父类的构造方法被调用,子类构造方法的剩余部分仍然会执行。子类构造方法不会因为调用了父类构造方法就停止执行。
示例:
package com.mohuanan.a007;
public class Employee {
//成员变量
private String id;
private String name;
private double salary;
//构造方法
public Employee() {
}
public Employee(String id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
//还有set() 和get() 方法
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
//成员方法
public void work(){
System.out.println("员工在工作");
}
public void eat(){
System.out.println("员工在吃饭");
}
}
package com.mohuanan.a007;
public class Cook extends Employee{
public Cook(){
}
public Cook(String id, String name, double salary) {
super(id, name, salary);
}
//重写父类里work方法
@Override
public void work() {
System.out.println("厨师在炒菜");
}
}
package com.mohuanan.a007;
public class Manager extends Employee{
//成员变量
private double bouns;
public Manager() {
}
public Manager(String id, String name, double salary, double bouns) {
super(id, name, salary);
this.bouns = bouns;
}
public double getBouns() {
return bouns;
}
public void setBouns(double bouns) {
this.bouns = bouns;
}
//重写父类里的work方法
@Override
public void work() {
System.out.println("管理其他人");
}
}
package com.mohuanan.a007;
public class Test {
public static void main(String[] args) {
//创建Manager对象 并使用有参构造 进行赋值
Manager m = new Manager("001","莫华南",15000,5000);
//调用里面的方法
System.out.println(m.getId()+","+m.getName()+","+
m.getSalary()+","+m.getBouns());
m.eat();
m.work();
System.out.println("------------------------");
//创建Cook对象
Cook c = new Cook();
c.setId("003");
c.setName("足球");
c.setSalary(9999);
System.out.println(c.getId()+","+c.getName()+","+c.getSalary());
c.eat();
c.work();
}
}
包
-
使用同一个包中的类时,不需要导包
-
使用Java.lang包的类时,不用导包
-
其他的情况都需要导包
-
如果同时使用两个包中的同名类,需要用全类名
在Java中,包(Package)是一种用于组织类和接口的方式。它类似于操作系统中的文件夹,可以将具有相似功能或属性的类、接口等文件组织在一起,从而方便管理和维护。包的主要作用有以下几点:
- 避免命名冲突:在Java中,不同的包可以有相同类名的类。通过使用包,我们可以区分这些具有相同类名的类,从而避免命名冲突。
- 控制访问权限:通过包,我们可以控制类和接口的访问权限。例如,如果一个类被声明为包私有(即没有访问修饰符),则它只能被同一个包中的其他类访问。这有助于保护类的内部实现细节,防止被外部类随意访问和修改。
- 组织代码结构:通过将相关的类和接口组织在同一个包中,我们可以使代码结构更加清晰和易于理解。这有助于开发者快速找到所需的类和方法,提高开发效率。
- 提供代码重用:通过将通用的类和接口组织成包,我们可以方便地在其他项目中重用这些代码。这有助于减少重复劳动,提高代码质量。
- 模块化开发:包可以作为模块化的基础,将功能相似的类组织在一起,形成一个独立的模块。这有助于实现代码的模块化开发,提高系统的可维护性和可扩展性。
在Java中,包的声明通常位于源文件的开头,使用package
关键字。例如,package com.example.myapp;
表示该源文件属于com.example.myapp
包。在编译源文件时,Java编译器会根据包名生成相应的目录结构,以便在运行时正确加载类。
需要注意的是,包名通常采用反向域名的方式命名,这是为了避免包名冲突,并确保包名的唯一性。例如,如果某个公司的域名为example.com
,那么它可能会将其包名命名为com.example.myapp
。
在Java中,import
和package
是两个不同的关键字,它们各自有特定的用途,不是同一个东西。
package
package
关键字用于声明一个类所在的包。包是Java中用于组织类和其他类型的机制,有助于避免命名冲突、控制访问权限,并提供了更好的代码模块化。通过将相关的类组织在同一个包中,可以提高代码的可读性和可维护性。
使用package
声明的包名通常反映了类的逻辑分组或项目的组织结构。例如,如果你有一个项目是关于学校管理的,你可能会使用com.school.management
作为包名。
import
import
关键字用于导入其他包中的类,这样在当前类中就可以直接使用这些类的名称,而不需要使用完整的包名前缀。import
语句有助于简化代码,提高可读性。
例如,如果你想在代码中使用java.util.ArrayList
类,你可以使用以下import
语句:
java复制代码import java.util.ArrayList;
之后,你就可以直接使用ArrayList
而不是java.util.ArrayList
。
你也可以使用星号(*)来导入包中的所有类,但这种做法通常不推荐,因为它可能导入不必要的类,增加编译时间,并可能导致命名冲突。
java复制代码import java.util.*; // 导入java.util包中的所有类
用途与区别
package
和import
在Java中各自扮演不同的角色:
package
用于声明类所在的包,是类的命名空间,有助于组织和管理代码。import
用于导入其他包中的类,使得在当前类中可以直接使用这些类的名称,而无需使用完整的包名前缀。
因此,它们不是同一个东西,而是Java中用于不同目的的两个关键字。在编写Java程序时,通常会首先使用package
声明类所在的包,然后根据需要使用import
语句导入所需的类。
final
-
final修饰的变量是基本数据类型,那么变量存储的数据值不能发生改变
-
final修饰的变量是引用数据类型,那么变量存储的地址值不能发生改变,对象内部的可以改变
在Java中,final
是一个关键字,它有多种用途,主要用于声明不可变的变量、不可继承的类以及不可覆盖的方法。以下是关于final
的详细解释:
定义与用途
- 不可变变量:当变量被
final
修饰时,它的值在初始化后就不能再被修改。这样的变量通常被称为常量,并且其命名习惯是全大写字母。 - 不可继承类:当类被
final
修饰时,它不能被其他类继承。这意味着其他类不能扩展或复用这个被final
修饰的类的功能。 - 不可覆盖方法:当方法被
final
修饰时,它不能被子类重写(覆盖)。这保证了父类中的方法行为在子类中不会被改变,从而增强了程序的稳定性和安全性。
使用方法
- 修饰变量:在声明变量时,使用
final
关键字修饰它。例如:final int COUNT = 10;
- 修饰类:在类声明前,使用
final
关键字。例如:final class MyClass { ... }
- 修饰方法:在方法声明前,使用
final
关键字。例如:final void myMethod() { ... }
注意事项
- 初始化:对于
final
修饰的变量,如果声明时没有进行初始化,那么在使用前必须被赋予初始值,否则会引发编译错误。 - 引用类型变量:当
final
修饰引用数据类型(如数组、字符串、集合等)时,它表示的是引用本身不可变,即不能指向其他对象,但引用的对象内部状态是可以改变的。 - 谨慎使用:过度使用
final
可能会使代码变得僵硬,不易扩展。因此,在决定使用final
时,应仔细考虑其是否真的必要。
总的来说,final
在Java中是一个强大的工具,用于确保变量的不变性、类的不可继承性以及方法的不可覆盖性。但同时,使用它时也需要谨慎考虑其可能带来的限制和影响。
权限修饰符
在Java中,权限修饰符是用于控制类、接口、方法和变量等成员的访问权限和可见性的关键字。它们决定了哪些代码可以访问和使用这些成员。使用权限修饰符可以提高代码的安全性和封装性,确保只有合适的代码能够访问和修改特定的成员。
Java中提供了四种权限修饰符,分别是:
- public:表示公共的,可以被任何类在任何地方访问。它是最高级别的访问权限。
- protected:表示受保护的,可以被同一包内的其他类访问,也可以被不同包中的子类访问。在继承关系中有特殊作用,子类可以访问父类的protected成员。
- 默认(无修饰符):也被称为包级访问权限,即只能被同一包中的其他类访问。如果没有使用任何访问权限修饰符,默认访问权限会被应用。
- private:表示私有的,只能在定义该成员的类内部访问。它用于隐藏类内部的实现细节,提供封装和数据安全性。
在使用这些权限修饰符时,需要注意以下事项:
private
修饰符不能用于修饰类,因为类本身不能被私有化。protected
修饰的成员在子类中可以访问,即使子类位于不同的包中。- 默认情况下,如果不使用任何修饰符,成员的访问权限为包级访问权限。
- 在编写代码时,建议根据成员的性质和需要选择合适的权限修饰符。通常,成员变量使用
private
隐藏细节,构造方法和成员方法使用public
方便创建对象和调用方法。
总结起来,Java中有四个权限修饰符,它们分别控制不同级别的访问权限,根据代码的需求和安全性考虑,应合理选择使用。
在Java中,final
和static
确实不是权限修饰符。它们各自有不同的用途和含义,用于控制变量的行为或改变方法或类的特性。
final
关键字主要有三个用途:
-
修饰变量:表示变量的值不能被重新赋值。如果
final
变量是一个基本数据类型的变量,那么它的值就不能被改变。如果final
变量是一个引用类型的变量,那么它只能指向初始化的那个对象,不能再指向其他对象。 -
修饰方法:表示该方法不能被重写(override)。这在父类中定义不希望被子类改变的方法时非常有用。
-
修饰类:表示该类不能被继承。这常用于工具类或者一些不希望被继承的类。
static
关键字也有几个主要用途:
-
修饰变量:表示该变量属于类本身,而不是类的实例。静态变量在内存中只有一份拷贝,无论创建多少个对象,静态变量都只有一份。静态变量也称为类变量,可以通过类名直接访问。
-
修饰方法:表示该方法属于类本身,而不是类的实例。静态方法可以直接通过类名调用,不需要创建对象。静态方法只能访问静态变量或静态方法,不能访问非静态变量或非静态方法。
-
修饰代码块:静态代码块是类加载时执行的代码块,只执行一次。它常用于初始化静态变量或执行只需执行一次的代码。
虽然final
和static
与权限修饰符(如public
、protected
、private
)在语法上都是放在类型或成员声明之前的,但它们的作用和意图是不同的。权限修饰符控制的是成员的访问权限,而final
和static
控制的是成员的生命周期和行为。
因此,当我们谈论Java的修饰符时,需要区分权限修饰符(控制访问权限)和非权限修饰符(如final
和static
,控制行为或生命周期)。
代码块
在Java中,代码块(也称为初始化块或构造块)是类的一部分,它在类实例化时自动执行。代码块不属于任何方法,而是直接定义在类体中。Java中的代码块主要有四种类型:静态代码块(static block)、实例初始化块(instance initialization block)、构造方法(constructor)和同步代码块(synchronized block)。这里我们主要讨论静态代码块和实例初始化块。
静态代码块
- 只执行一次
在Java中,静态代码块(Static Block)是一个特殊的代码块,它用static
关键字来标记,并包含在类定义中,但在任何方法之外。静态代码块在类被加载到JVM时执行,且只执行一次。
静态代码块的用处:
-
初始化静态变量:静态代码块常用于初始化类的静态变量,尤其是当初始化过程涉及复杂的逻辑或需要执行某些操作时。
-
执行只需在类加载时执行一次的代码:例如,加载配置文件、建立数据库连接等。
-
设置类的静态状态:在类的所有实例被创建之前,设置类的静态状态。
如何使用静态代码块:
在类中定义方法之外,使用static
关键字来标记一个代码块,如下所示:
public class MyClass {
static {
// 这里是静态代码块的内容
System.out.println("静态代码块执行了");
// 可以进行静态变量的初始化或执行其他只需要在类加载时执行的操作
}
// 类的其他成员(静态变量、实例变量、方法等)
public static void main(String[] args) {
// 当main方法被执行时,如果MyClass类还没有被加载,那么静态代码块会首先执行
System.out.println("主方法执行了");
}
}
-
执行顺序:静态代码块按照它们在类中出现的顺序执行。如果有多个静态代码块,它们会按照在源代码中从上到下的顺序依次执行。
-
访问权限:静态代码块只能访问静态变量或静态方法,因为它们在类加载时执行,此时类的实例尚未创建,因此无法访问实例变量或实例方法。
-
异常处理:静态代码块中抛出的任何未检查的异常(runtime exceptions)都会导致类加载失败。因此,需要确保静态代码块中的代码不会抛出未处理的异常。
-
性能考虑:静态代码块在类加载时执行,如果其中包含耗时操作,可能会导致类加载速度变慢,影响程序的启动时间。
-
多线程安全:静态代码块在类加载时由单个线程执行,因此是线程安全的。但是,如果在静态代码块中初始化静态变量时涉及到多线程访问共享资源,仍然需要确保线程安全。
-
避免在静态代码块中创建过多的对象:静态代码块在类加载时执行,如果在这里创建了大量的对象,会消耗大量的内存,可能会影响程序的性能。
总之,静态代码块在Java中是一种非常有用的工具,可以在类加载时执行一些必要的初始化操作。但是,使用时需要注意其执行顺序、访问权限、异常处理以及可能对性能产生的影响。
抽象类
-
抽象方法的定义格式: public abstract 返回值类型 方法名(参数列表); 注意不写方法体
-
抽象类的定义格式: public abstract class 类名{}
在Java中,抽象类是一种特殊的类,它不能被实例化(即不能创建对象)。抽象类主要用于定义一种抽象的概念或者一个接口,其子类可以根据需要实现这些抽象概念或接口的具体细节。
抽象类的用处:
-
提供通用接口:抽象类可以定义一组公共的方法或属性,子类可以继承这些方法和属性,从而实现代码的复用。
-
定义规范:抽象类可以定义一些抽象方法,这些方法在抽象类中没有具体的实现,子类必须提供这些方法的具体实现,这有助于定义一种统一的接口规范。
-
隐藏实现细节:通过抽象类和具体子类的划分,可以将实现细节隐藏在具体子类中,抽象类只暴露必要的接口,提高了代码的安全性和可维护性。
如何使用抽象类:
-
定义抽象类:使用
abstract
关键字来定义一个抽象类。 -
定义抽象方法:在抽象类中,可以使用
abstract
关键字来定义抽象方法,抽象方法没有方法体。 -
创建子类:子类需要继承抽象类,并实现抽象类中定义的所有抽象方法。如果子类没有实现所有的抽象方法,那么子类也必须被声明为抽象类。
-
实例化子类:由于抽象类不能被实例化,所以我们需要实例化子类来使用抽象类定义的功能。
示例:
// 定义抽象类
abstract class Animal {
abstract void makeSound(); // 抽象方法
void eat() { // 普通方法
System.out.println("Animal eats");
}
}
// 创建子类并实现抽象方法
class Dog extends Animal {
@Override
void makeSound() { // 实现抽象方法
System.out.println("Dog barks");
}
}
// 主类
public class Main {
public static void main(String[] args) {
// 实例化子类
Dog dog = new Dog();
dog.makeSound(); // 输出 "Dog barks"
dog.eat(); // 输出 "Animal eats"
}
}
注意事项:
-
抽象方法必须被实现:如果一个类继承了一个抽象类,它必须实现该抽象类中的所有抽象方法,除非它本身也是一个抽象类。
-
抽象类不能实例化:尝试实例化一个抽象类将会导致编译错误。
-
构造器:抽象类可以有构造器,这通常用于初始化抽象类的通用状态,供子类使用。
-
抽象类可以有非抽象方法和字段:抽象类不仅可以包含抽象方法,还可以包含非抽象方法和字段。
-
final和abstract不能同时使用:
final
类不能被继承,而抽象类是为了被继承的,因此final
和abstract
不能同时修饰一个类。同样,final
方法和abstract
方法也是互斥的。 -
接口与抽象类的选择:接口是完全抽象的,只能定义抽象方法,而抽象类可以有抽象方法和非抽象方法。接口更适合定义一组行为规范,而抽象类更适合定义一组具有共同行为的对象的基类。
接口
在Java中,接口(Interface)是一种引用类型,它是方法的集合,可以被类实现。接口不能被实例化,即不能创建接口的对象。接口定义了一种规范或契约,要求实现接口的类必须遵循这个规范,提供接口中声明的方法的具体实现。
接口的用处:
-
定义规范:接口定义了一组方法的规范,实现接口的类必须提供这些方法的具体实现。这有助于保证代码的一致性和可维护性。
-
解耦:接口允许我们将依赖关系建立在接口上,而不是具体的实现类上。这样,我们可以轻松地替换实现,而不影响其他代码。
-
实现多态:接口是实现多态性的重要手段。通过接口,我们可以将不同类型的对象当作同一类型来处理,提高了代码的灵活性和可扩展性。
-
支持回调:接口经常用于定义回调方法,实现某种通知机制。
如何使用接口:
- 定义接口:使用
interface
关键字来定义一个接口,接口中可以声明常量(默认是public static final
)和抽象方法(默认是public abstract
)。
public interface MyInterface {
void method1();
default void method2() {
// Java 8开始,接口可以有默认方法实现
}
static void method3() {
// Java 8开始,接口可以有静态方法
}
}
- 实现接口:一个类使用
implements
关键字来实现一个接口,并提供接口中所有方法的具体实现。
public class MyClass implements MyInterface {
@Override
public void method1() {
// 实现接口中的方法
}
// method2() 不需要实现,因为它是默认方法
// method3() 是静态方法,不需要在类中实现
}
- 使用接口引用:可以创建接口的引用,并指向实现了该接口的类的对象。
MyInterface obj = new MyClass();
obj.method1(); // 调用接口方法
注意事项:
-
接口不能实例化:尝试创建接口的对象会导致编译错误。
-
接口中的方法默认是抽象的:在接口中声明方法时,不需要使用
abstract
关键字。 -
接口中不能有构造器:接口不是类,所以没有构造器。
-
接口可以有默认方法和静态方法:从Java 8开始,接口可以包含默认方法和静态方法。默认方法允许在接口中提供方法的具体实现,而静态方法则可以直接通过接口名调用。
-
实现接口的类必须实现接口中的所有抽象方法:否则该类也必须声明为抽象类。
-
一个类可以实现多个接口:通过逗号分隔接口名,可以在一个类中实现多个接口。
-
接口可以继承其他接口:使用
extends
关键字,一个接口可以继承其他一个或多个接口,从而继承它们的方法。 -
接口与抽象类的选择:接口通常用于定义一组行为的规范,而抽象类则更适合定义一组具有共同行为的对象的基类。在选择使用接口还是抽象类时,需要考虑具体的设计需求和场景。
示例代码:
Person类
package com.mohuanan.Test03;
//目的为了不让外界创建Person对象
//因为创建了是没有意义的
//所以我就把他定义为抽象类 abstract
public abstract class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = 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;
}
}
Athlete类
package com.mohuanan.Test03;
public abstract class Athlete extends Person{
public Athlete() {
}
public Athlete(String name, int age) {
super(name, age);
}
public abstract void study();
}
Coach类
package com.mohuanan.Test03;
public abstract class Coach extends Person{
public Coach() {
}
public Coach(String name, int age) {
super(name, age);
}
public abstract void teach();
}
BacketAthlete类
package com.mohuanan.Test03;
public class BasketAthlete extends Athlete{
public BasketAthlete() {
}
public BasketAthlete(String name, int age) {
super(name, age);
}
@Override
public void study() {
System.out.println("学习打篮球");
}
}
PingpongAthlete类
package com.mohuanan.Test03;
public class PingpongAthlete extends Athlete implements Speak{
public PingpongAthlete() {
}
public PingpongAthlete(String name, int age) {
super(name, age);
}
@Override
public void study() {
System.out.println("学习乒乓球");
}
@Override
public void speak() {
System.out.println("说英语");
}
}
BacketCoach类
package com.mohuanan.Test03;
public class BasketCoach extends Coach{
public BasketCoach() {
}
public BasketCoach(String name, int age) {
super(name, age);
}
@Override
public void teach() {
System.out.println("教篮球");
}
}
PingpongCoach类
package com.mohuanan.Test03;
public class PingpongCoach extends Coach implements Speak{
public PingpongCoach() {
}
public PingpongCoach(String name, int age) {
super(name, age);
}
@Override
public void teach() {
System.out.println("教打乒乓球");
}
@Override
public void speak() {
System.out.println("说英语");
}
}
Speak接口
package com.mohuanan.Test03;
public interface Speak {
public abstract void speak();
}
Test类
package com.mohuanan.Test03;
public class Test {
public static void main(String[] args) {
//创建对象 并赋值
PingpongAthlete pa = new PingpongAthlete("莫华南",18);
System.out.println(pa.getAge()+","+pa.getName());
pa.speak();
pa.study();
System.out.println("----------------");
BasketCoach bc = new BasketCoach("莫华棋",10);
System.out.println(bc.getAge()+","+bc.getName());
bc.teach();
//bc.speak();
}
}
多态
在Java中,多态(Polymorphism)是面向对象编程的三大特性之一,另外两个是封装(Encapsulation)和继承(Inheritance)。多态意味着一个接口可以有多种不同的实现方式或者一个引用类型可以指向多个不同类型的对象。
具体来说,多态主要有两种形式:
- 方法的重载(Overloading):在同一个类中,可以有多个名称相同但参数列表不同的方法。这允许我们根据传入的参数类型或数量来执行不同的操作。
- 方法的重写(Overriding)与运行时多态:在子类中,我们可以重写父类中的方法。这样,当我们使用父类类型的引用来引用子类对象时,调用该方法将执行子类中的版本,而不是父类中的版本。这种多态性通常与继承一起使用,并在运行时确定要执行的具体方法,因此也被称为运行时多态。
多态的主要用处包括:
- 代码复用:通过多态,我们可以使用父类类型的引用来引用不同的子类对象,从而执行不同的操作。这使得我们可以编写更通用的代码,减少重复的代码量。
- 扩展性:多态允许我们在不修改现有代码的情况下添加新的子类。这使得我们的代码更加灵活和可扩展。
- 接口实现:多态使得我们可以定义接口并让不同的类实现这些接口。这样,我们可以使用接口类型的引用来引用任何实现了该接口的类的对象,从而实现更高级的抽象和模块化。
- 简化程序设计:通过多态,我们可以将复杂的问题分解为更简单、更易于管理的部分。我们可以将不同的行为封装在不同的子类中,并通过父类类型的引用来统一处理这些行为。
总的来说,多态是Java中一种非常强大的特性,它使得我们的代码更加灵活、可扩展和易于维护。
在Java中,当你声明一个变量为某个父类类型,但是实际上你给它分配了一个子类对象时,该变量仍然只保留父类类型的引用。这意味着,虽然它指向的对象具有子类的所有属性和方法,但是通过父类类型的引用,你只能访问父类中定义的方法(除非进行了类型转换)。这就是为什么在运行时可能需要进行类型检查和转换的原因。
在你给出的例子中:
java复制代码Animal a = new Dog();
变量a
被声明为Animal
类型,但是实际上它引用了一个Dog
对象。因为a
是Animal
类型,所以通过a
你只能调用Animal
类中定义的方法。如果Dog
类有一些特有的方法(比如lookHome()
),那么你不能直接通过a
来调用这些方法,除非你首先将a
转换为Dog
类型。
即使你知道a
实际上指向了一个Dog
对象,Java编译器在编译时并不知道这一点。编译器只关心变量的声明类型。因此,为了安全起见,并且让代码在运行时能够正常工作,你需要进行类型检查(使用instanceof
)和类型转换(使用强制类型转换)。
类型检查a instanceof Dog
确保a
确实引用了一个Dog
对象,而不是其他类型的Animal
对象。如果a
不是Dog
的实例,那么尝试进行类型转换会导致ClassCastException
。通过先进行检查,你可以避免这种运行时异常。
一旦确认a
是Dog
的实例,你就可以安全地将它转换为Dog
类型,并调用Dog
类特有的方法。这是通过强制类型转换(Dog) a
完成的。
总结来说,即使你知道a
实际上指向了Dog
对象,你也需要进行类型检查和转换,因为Java是一种静态类型语言,它在编译时根据变量的声明类型来限制你可以调用的方法。在运行时,通过类型检查和转换,你可以确保安全地访问对象的实际类型的方法。
重要
Animal类
package com.mohuanan.a010;
public class Animal {
private int age;
private String corlor;
public Animal() {
}
public Animal(int age, String corlor) {
this.age = age;
this.corlor = corlor;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getCorlor() {
return corlor;
}
public void setCorlor(String corlor) {
this.corlor = corlor;
}
public void eat(String something){
System.out.println("动物在吃"+something);
}
}
Dog类
package com.mohuanan.a010;
public class Dog extends Animal{
public Dog() {
}
public Dog(int age, String corlor) {
super(age, corlor);
}
@Override
public void eat(String something) {
System.out.println(getAge()+"岁的"+getCorlor()+"颜色的狗两只前腿死死的抱住"+something+"猛吃");
}
public void lookHome(){
System.out.println("狗在看家");
}
}
Cat类
package com.mohuanan.a010;
public class Cat extends Animal{
public Cat() {
}
public Cat(int age, String corlor) {
super(age, corlor);
}
@Override
public void eat(String something) {
System.out.println(getAge()+"岁的"+getCorlor()+"颜色的猫两只前腿死死的抱住"+something+"猛吃");
}
public void catchMouse(){
System.out.println("猫在捉老鼠");
}
}
Person类
package com.mohuanan.a010;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = 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;
}
//使用多态
public void keepPet(Animal a,String something){
if(a instanceof Dog){
//强制转化
Dog d = (Dog) a;
System.out.println("年龄为"+this.age+"岁的"+this.name+"养了一只"+d.getCorlor()+"颜色的"+d.getAge()+"岁的狗");
//调用重写的eat()方法
d.eat(something);
//调用特有的方法
d.lookHome();
}else if(a instanceof Cat){
//强制转化
Cat c = (Cat) a;
System.out.println("年龄为"+this.age+"岁的"+this.name+"养了一只"+c.getCorlor()+"颜色的"+c.getAge()+"岁的猫");
//调用重写的eat()方法
c.eat(something);
//调用特有的方法
c.catchMouse();
}else{
System.out.println("父类没有继承这个对象");
}
}
}
Test类
package com.mohuanan.a010;
public class Test {
public static void main(String[] args) {
//创建对象
Person p1 = new Person("老王",30);
Dog d = new Dog(2,"黑");
p1.keepPet(d,"骨头");
Person p2 = new Person("老李",25);
Cat c = new Cat(3,"灰");
p2.keepPet(c,"小鱼");
}
}
输出结果:
内部类
在Java中,内部类(Inner Class)是定义在另一个类(外部类)内部的类。内部类可以访问外部类的所有成员,包括私有成员,同时外部类也可以访问内部类的所有成员。内部类主要有四种类型:静态内部类(Static Inner Class)、成员内部类(Member Inner Class)、局部内部类(Local Inner Class)和匿名内部类(Anonymous Inner Class)。
内部类的主要用途包括:
- 封装性:内部类可以隐藏起来,不被同一个包中的其他类访问。
- 实现多重继承:Java不支持类的多重继承,但可以通过内部类实现接口,从而达到多重继承的效果。
- 回调和事件监听:内部类常用于实现回调和事件监听机制,例如Android中的事件处理。
内部类的使用方法如下:
public class OuterClass {
// 成员内部类
class MemberInnerClass {
void display() {
System.out.println("Inside MemberInnerClass");
}
}
// 静态内部类
static class StaticInnerClass {
void display() {
System.out.println("Inside StaticInnerClass");
}
}
// 方法内部类(局部内部类)
public void someMethod() {
class LocalInnerClass {
void display() {
System.out.println("Inside LocalInnerClass");
}
}
LocalInnerClass lic = new LocalInnerClass();
lic.display();
}
public static void main(String[] args) {
OuterClass oc = new OuterClass();
OuterClass.MemberInnerClass mic = oc.new MemberInnerClass();
mic.display();
OuterClass.StaticInnerClass sic = new OuterClass.StaticInnerClass();
sic.display();
oc.someMethod(); // 调用方法,从而调用局部内部类
}
}
使用内部类时需要注意以下几点:
- 访问权限:内部类可以直接访问外部类的所有成员,包括私有成员。但外部类要访问内部类的成员,必须通过内部类的对象来访问。
- 静态成员:如果内部类要声明为静态的,那么它就不能访问外部类的非静态成员。因为静态成员是属于类的,而非静态成员是属于对象的,静态内部类在创建时不需要外部类对象。
- 命名冲突:如果内部类的名称与外部类的成员名称相同,那么内部类会隐藏外部类的成员。
- 序列化:如果外部类实现了Serializable接口,那么内部类也会被序列化。如果不想内部类被序列化,可以给它单独实现Serializable接口,并定义一个空的序列化ID字段。
- 局部内部类和匿名内部类:局部内部类只能在声明它的方法或代码块中访问和使用,一旦出了那个范围就不能再被访问。匿名内部类通常用于实现接口或继承类,并立即创建该类的实例。
以上就是Java内部类的基本概念、用途、使用方法和注意事项。
匿名内部类
在Java中,匿名内部类(Anonymous Inner Class)是没有名称的局部内部类,通常用于一次性使用的情况,如创建一个实现了某个接口或继承了某个类的匿名对象。匿名内部类常用于事件处理、多线程编程等场景。
匿名内部类的用途:
- 简化代码:匿名内部类通常用于创建一次性使用的对象,从而避免编写额外的类或接口。
- 实现接口或继承类:匿名内部类可以用来实现一个或多个接口,或者继承一个类,并覆盖其中的方法。
匿名内部类的使用:
匿名内部类通常用于以下几种情况:
- 实现接口:
interface Greeting {
void sayHello();
}
public class Main {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
@Override
public void sayHello() {
System.out.println("Hello, World!");
}
};
greeting.sayHello();
}
}
- 继承类并覆盖方法:
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Animal() {
@Override
void makeSound() {
System.out.println("The dog barks");
}
};
dog.makeSound();
}
}
注意事项:
-
只能使用一次:匿名内部类通常只被实例化一次,因为它没有名称,所以无法在其他地方引用。
-
无法声明构造函数:匿名内部类没有名字,因此不能声明构造函数。如果需要初始化,可以在定义时直接进行初始化。
-
外部类的访问:匿名内部类可以访问外部类的所有成员,包括私有成员。
-
变量引用:在匿名内部类中,不能引用外部方法的局部变量(除非这些变量是final或等效于final的)。这是因为局部变量存储在栈上,而匿名内部类对象可能存储在堆上,局部变量的生命周期可能与匿名内部类对象不一致。
-
类型推断:匿名内部类的类型通常由其实现的接口或继承的类来推断,因此不需要显式地指定类型。
-
可读性和维护性:虽然匿名内部类可以简化代码,但如果使用过多,可能会降低代码的可读性和维护性。在可能的情况下,使用常规的类或接口定义可能更加清晰。
-
序列化:如果外部类是可序列化的,那么匿名内部类默认也是可序列化的。如果不需要序列化,可以显式地使匿名内部类不可序列化。
总的来说,匿名内部类是Java提供的一种灵活且强大的特性,允许开发者在需要时快速创建一次性使用的对象。然而,在使用时需要注意其局限性和潜在的问题,以确保代码的健壮性和可维护性。