MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java访问控制符与类的设计原则

2022-09-073.4k 阅读

Java访问控制符

在Java编程中,访问控制符扮演着至关重要的角色,它决定了类、方法、变量等成员的访问权限,有效保障了代码的安全性和封装性。Java提供了四种访问控制符:private、default(默认,无关键字)、protected和public,它们在不同的场景下发挥着各自的作用。

private访问控制符

private访问控制符是最严格的访问级别,被其修饰的成员只能在当前类内部访问。这意味着类的外部,包括其他类以及该类的子类,都无法直接访问这些成员。

class PrivateExample {
    private int privateVariable;

    private void privateMethod() {
        System.out.println("This is a private method.");
    }

    // 类内部访问private成员
    public void accessPrivateMembers() {
        privateVariable = 10;
        privateMethod();
    }
}

public class Main {
    public static void main(String[] args) {
        PrivateExample example = new PrivateExample();
        // 以下代码将导致编译错误
        // example.privateVariable = 20;
        // example.privateMethod();
        example.accessPrivateMembers();
    }
}

在上述代码中,privateVariableprivateMethod被声明为private,在Main类中直接访问它们会导致编译错误。而在PrivateExample类内部,accessPrivateMembers方法可以正常访问这些私有成员。

使用private访问控制符的主要目的是实现数据封装,将类的内部状态和实现细节隐藏起来,只对外提供必要的接口方法,这样可以避免外部代码对类的内部数据造成意外修改,提高代码的安全性和稳定性。

default访问控制符(默认访问级别)

当一个类、方法或变量没有显式地使用任何访问控制符时,它就具有默认访问级别,也称为包访问级别。具有默认访问级别的成员可以被同一个包内的其他类访问,但不能被其他包中的类访问,即使是子类也不行,除非子类位于同一个包中。

package com.example.package1;

class DefaultExample {
    int defaultVariable;

    void defaultMethod() {
        System.out.println("This is a default method.");
    }
}

class AnotherClassInSamePackage {
    public void accessDefaultMembers() {
        DefaultExample example = new DefaultExample();
        example.defaultVariable = 20;
        example.defaultMethod();
    }
}

package com.example.package2;

public class DifferentPackageClass {
    public void tryAccessDefaultMembers() {
        com.example.package1.DefaultExample example = new com.example.package1.DefaultExample();
        // 以下代码将导致编译错误
        // example.defaultVariable = 30;
        // example.defaultMethod();
    }
}

在上述代码中,DefaultExample类中的defaultVariabledefaultMethod具有默认访问级别。AnotherClassInSamePackage类与DefaultExample类在同一个包中,因此可以访问这些默认成员。而DifferentPackageClass类位于不同的包中,试图访问DefaultExample类的默认成员会导致编译错误。

默认访问级别适用于那些只在同一个包内使用的类和成员,它提供了一定程度的封装,同时又允许包内的类之间进行合理的交互。

protected访问控制符

protected访问控制符介于default和public之间。被protected修饰的成员可以被同一个包内的其他类访问,也可以被不同包中的子类访问。

package com.example.package1;

class ProtectedExample {
    protected int protectedVariable;

    protected void protectedMethod() {
        System.out.println("This is a protected method.");
    }
}

package com.example.package2;

class Subclass extends com.example.package1.ProtectedExample {
    public void accessProtectedMembers() {
        protectedVariable = 40;
        protectedMethod();
    }
}

package com.example.package2;

class OtherClassInDifferentPackage {
    public void tryAccessProtectedMembers() {
        com.example.package1.ProtectedExample example = new com.example.package1.ProtectedExample();
        // 以下代码将导致编译错误
        // example.protectedVariable = 50;
        // example.protectedMethod();
    }
}

在上述代码中,ProtectedExample类中的protectedVariableprotectedMethod被声明为protectedSubclass类是ProtectedExample类的子类,虽然位于不同的包中,但仍然可以访问这些受保护的成员。而OtherClassInDifferentPackage类不是子类,即使在不同包中也不能直接访问ProtectedExample类的受保护成员。

protected访问控制符常用于实现继承体系中的成员访问控制,它允许子类继承并扩展父类的功能,同时又限制了非子类对这些成员的访问。

public访问控制符

public访问控制符是最宽松的访问级别,被其修饰的类、方法或变量可以被任何其他类访问,无论这些类位于哪个包中。

public class PublicExample {
    public int publicVariable;

    public void publicMethod() {
        System.out.println("This is a public method.");
    }
}

public class Main {
    public static void main(String[] args) {
        PublicExample example = new PublicExample();
        example.publicVariable = 60;
        example.publicMethod();
    }
}

在上述代码中,PublicExample类中的publicVariablepublicMethod被声明为public,在Main类中可以直接访问这些公共成员,无论它们是否在同一个包中。

通常,将类声明为public表示该类是一个公共的API,可供其他开发者使用。而将方法和变量声明为public则表示它们是类对外提供的公开接口,应该谨慎使用,因为过度暴露内部实现可能会破坏封装性和安全性。

类的设计原则

在Java编程中,遵循良好的类设计原则对于构建可维护、可扩展和健壮的软件系统至关重要。以下是一些重要的类设计原则:

单一职责原则(SRP)

单一职责原则指出,一个类应该只有一个引起它变化的原因,也就是说一个类应该只负责一项职责。如果一个类承担了过多的职责,当其中一项职责发生变化时,可能会影响到其他职责的功能,导致代码的维护和扩展变得困难。

// 违反单一职责原则的示例
class BadUserService {
    public void saveUser(String username, String password) {
        // 保存用户到数据库的逻辑
        System.out.println("Saving user " + username + " with password " + password + " to database.");
    }

    public void sendWelcomeEmail(String username) {
        // 发送欢迎邮件的逻辑
        System.out.println("Sending welcome email to " + username + ".");
    }
}

// 遵循单一职责原则的示例
class UserDatabaseService {
    public void saveUser(String username, String password) {
        System.out.println("Saving user " + username + " with password " + password + " to database.");
    }
}

class UserEmailService {
    public void sendWelcomeEmail(String username) {
        System.out.println("Sending welcome email to " + username + ".");
    }
}

在上述代码中,BadUserService类承担了保存用户到数据库和发送欢迎邮件两项职责,违反了单一职责原则。而UserDatabaseServiceUserEmailService分别负责单一的职责,符合该原则。这样,当数据库存储逻辑或邮件发送逻辑发生变化时,只需要修改对应的类,不会影响到其他功能。

开闭原则(OCP)

开闭原则强调软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的前提下,可以通过扩展来添加新的功能。

// 图形类
abstract class Shape {
    abstract double getArea();
}

// 圆形类
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double getArea() {
        return Math.PI * radius * radius;
    }
}

// 矩形类
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double getArea() {
        return width * height;
    }
}

// 计算图形面积总和的类,遵循开闭原则
class AreaCalculator {
    public double calculateTotalArea(Shape[] shapes) {
        double totalArea = 0;
        for (Shape shape : shapes) {
            totalArea += shape.getArea();
        }
        return totalArea;
    }
}

在上述代码中,AreaCalculator类通过依赖抽象的Shape类来计算图形的总面积。当需要添加新的图形(如三角形)时,只需要创建一个继承自Shape类的Triangle类,并实现getArea方法,而不需要修改AreaCalculator类的代码,符合开闭原则。

里氏替换原则(LSP)

里氏替换原则要求任何基类可以出现的地方,子类一定可以出现,并且替换为子类后不会改变程序的正确性。这意味着子类必须完全实现父类的接口,并且行为要与父类保持一致。

class Rectangle {
    private double width;
    private double height;

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public double getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(5);
        rectangle.setHeight(10);
        System.out.println("Rectangle area: " + rectangle.getArea());

        Rectangle square = new Square();
        square.setWidth(5);
        System.out.println("Square area: " + square.getArea());
    }
}

在上述代码中,Square类继承自Rectangle类,并重新实现了setWidthsetHeight方法以保持正方形的特性。在Main类中,可以将Square对象当作Rectangle对象使用,并且程序的行为是正确的,符合里氏替换原则。

依赖倒置原则(DIP)

依赖倒置原则提倡高层模块不应该依赖底层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。这有助于降低模块之间的耦合度,提高代码的可维护性和可扩展性。

// 抽象的消息发送接口
interface MessageSender {
    void sendMessage(String message);
}

// 具体的邮件发送类,实现消息发送接口
class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

// 具体的短信发送类,实现消息发送接口
class SmsSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// 消息服务类,依赖抽象的消息发送接口
class MessageService {
    private MessageSender messageSender;

    public MessageService(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void send(String message) {
        messageSender.sendMessage(message);
    }
}

public class Main {
    public static void main(String[] args) {
        MessageSender emailSender = new EmailSender();
        MessageService emailService = new MessageService(emailSender);
        emailService.send("This is an email.");

        MessageSender smsSender = new SmsSender();
        MessageService smsService = new MessageService(smsSender);
        smsService.send("This is an SMS.");
    }
}

在上述代码中,MessageService类依赖抽象的MessageSender接口,而不是具体的EmailSenderSmsSender类。这样,当需要更换消息发送方式时,只需要创建一个新的实现MessageSender接口的类,并将其传递给MessageService类,而不需要修改MessageService类的代码,符合依赖倒置原则。

接口隔离原则(ISP)

接口隔离原则主张客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。这可以避免接口臃肿,提高代码的灵活性和可维护性。

// 抽象的打印接口
interface Printer {
    void printDocument();
}

// 抽象的扫描接口
interface Scanner {
    void scanDocument();
}

// 多功能一体机类,实现打印和扫描接口
class AllInOnePrinter implements Printer, Scanner {
    @Override
    public void printDocument() {
        System.out.println("Printing document.");
    }

    @Override
    public void scanDocument() {
        System.out.println("Scanning document.");
    }
}

// 简单打印机类,只实现打印接口
class SimplePrinter implements Printer {
    @Override
    public void printDocument() {
        System.out.println("Printing document.");
    }
}

在上述代码中,PrinterScanner接口分别定义了打印和扫描的功能。AllInOnePrinter类实现了这两个接口,而SimplePrinter类只实现了Printer接口。如果没有遵循接口隔离原则,将所有功能放在一个大接口中,SimplePrinter类就不得不实现它不需要的扫描功能,导致代码冗余和不必要的依赖。

通过遵循这些类设计原则,开发者可以构建出更加清晰、灵活和健壮的Java程序,提高代码的质量和可维护性,从而降低软件开发和维护的成本。同时,这些原则也是面向对象设计的基石,对于构建大型、复杂的软件系统具有重要的指导意义。在实际编程过程中,需要根据具体的业务需求和场景,灵活运用这些原则,以达到最佳的设计效果。例如,在设计一个大型的企业级应用时,可能需要综合考虑多个原则,将不同的功能模块进行合理的划分和设计,确保各个模块之间低耦合、高内聚,以适应不断变化的业务需求。而在一些小型的项目中,虽然可能不需要严格遵循所有原则,但了解并适当应用这些原则可以帮助开发者写出更易于理解和维护的代码。总之,掌握和运用这些类设计原则是成为一名优秀Java开发者的重要一步。

另外,在实际应用中,类的设计原则之间并不是孤立的,而是相互关联、相互影响的。例如,遵循单一职责原则有助于更好地实现开闭原则,因为当一个类只负责一项职责时,对其进行扩展时就更容易做到不修改现有代码。同样,里氏替换原则是依赖倒置原则和接口隔离原则的基础,只有确保子类能够正确替换父类,才能更好地实现依赖抽象和接口的合理设计。因此,在设计类时,需要全面考虑这些原则,权衡利弊,以达到最优的设计方案。同时,随着项目的不断演进和需求的变化,可能需要对类的设计进行调整和优化,以持续满足软件系统的可维护性和可扩展性要求。例如,在项目初期,可能由于对业务需求的理解不够深入,类的设计没有完全遵循某些原则,随着业务的发展,发现代码维护困难或扩展性不足,此时就需要根据类设计原则对代码进行重构,使代码结构更加合理。

此外,在遵循类设计原则的过程中,还需要注意与Java语言特性和编程习惯相结合。例如,Java的访问控制符在实现类设计原则中起到了重要的辅助作用。通过合理使用访问控制符,可以有效地隐藏类的内部实现细节,提高类的封装性,从而更好地遵循单一职责原则等。同时,Java的继承、接口等特性也为实现类设计原则提供了有力的支持。例如,通过继承和接口实现,可以方便地实现开闭原则和依赖倒置原则。然而,如果使用不当,这些特性也可能导致违反类设计原则的情况发生。比如,过度使用继承可能会导致类之间的耦合度增加,不符合依赖倒置原则。因此,开发者需要深入理解Java语言特性与类设计原则之间的关系,在编程过程中灵活运用,以构建出高质量的Java程序。

在团队开发中,统一遵循类设计原则也是非常重要的。团队成员之间对这些原则的理解和应用一致,可以使代码风格更加统一,提高代码的可读性和可维护性。同时,在代码审查过程中,以类设计原则为标准进行审查,可以及时发现代码中存在的问题,并进行改进。例如,在审查代码时,如果发现某个类承担了过多的职责,就可以根据单一职责原则提出改进建议,将该类拆分成多个职责单一的类。这样不仅有助于提高代码的质量,还可以促进团队成员之间的技术交流和能力提升。

在实际项目中,还可以结合一些设计模式来更好地应用类设计原则。设计模式是在大量实践中总结出来的通用解决方案,它们通常都遵循了类设计原则。例如,工厂模式可以帮助我们更好地实现依赖倒置原则,通过将对象的创建过程封装在工厂类中,使高层模块依赖于抽象的产品接口,而不是具体的产品类。策略模式则可以用于实现开闭原则,通过将不同的算法封装成具体的策略类,并实现一个抽象的策略接口,当需要添加新的算法时,只需要创建一个新的策略类实现该接口,而不需要修改原有代码。了解和应用这些设计模式,可以进一步提升我们对类设计原则的理解和应用能力,使我们能够更加高效地构建出符合要求的软件系统。

总之,Java访问控制符和类的设计原则是Java编程中非常重要的两个方面。访问控制符确保了类的成员具有合适的访问权限,实现了数据封装和安全性;而类的设计原则则指导我们构建出可维护、可扩展和健壮的软件系统。在实际编程中,深入理解并灵活运用这两个方面的知识,对于提高代码质量、降低开发成本以及应对不断变化的业务需求都具有至关重要的意义。无论是开发小型的桌面应用,还是大型的企业级分布式系统,遵循这些规则和原则都能够使我们的代码更加优秀,为软件项目的成功奠定坚实的基础。同时,随着技术的不断发展和软件需求的日益复杂,我们需要不断学习和实践,以更好地掌握和应用这些知识,适应新的挑战和机遇。例如,在当前云计算、大数据和人工智能等新兴技术领域,Java仍然是重要的开发语言之一,在这些领域的项目开发中,同样需要遵循访问控制符和类设计原则,以构建出高效、可靠的软件系统。因此,持续关注和深入研究这些内容,对于Java开发者来说是非常必要的。