Getter 和 Java 中的二传手

Getter and Setters in Java

在 getter 和 setter 中制作一些数据 processing/validation 是个好习惯吗?在维基百科 here 中有 2 个示例:

这些是很好的例子还是最好避免?如果最好避免它,我怎样才能更好地实现上面的这两个例子?

更新: 请不要认真对待带有日期等的例子。只是举个例子,当然可以很蠢

我有过的真实例子

我有一个外部第三方系统,我必须进行集成。这个外部系统期望从我这里得到一些数据,如 class 以及 getter 和 setter。我必须在那里传递 2 个字段,id(类似于 09df723987cd7(比如说 GUID))和 formattedID,类似于 "objecttype/09df723987cd7"。我无法更改此外部系统。

我想像

那样实现
getId() {
  return id
}

getFormattedId() {
    return objectType + "/" + id;
}

objectType 是此 class 中的另一个字段。

我的问题:是否可以,或者有更优雅的实现方式?

有点。有时 setter 方法可能需要采用不同的类型并将其按摩到 属性 的内部表示中。示例:

class Example {
    private Integer value;
    public void setValue(Integer value) {
        this.value = value;
    }
    public void setValue(String value) {
        try {
           this.value = Integer.parseInt(value);
        } catch (NumberFormatException nfe) {
            throw new IllegalArgumentException(String.format("%s contains no valid numeric string.", value));
        }
    }
}

接受 String 的重载 setValue 是一个有效的例子。但是,一般来说,适当的 getter 和 setter 将直接映射到 属性 的内部表示。因此,例如,setDate 示例将仅对 Date 对象进行操作并设置对象的内部 Date 属性,部分原因是小时、分钟等是Date 的所有部分和分解 Date 在这种情况下是一种不好的做法。

Getter和setter达到封装的目的。

例如,

public class Sample
{
     private int somedata; // Encapsulated
     public int Get() {return somedata;} //Getter
     public void Set(int var) {somedata = var;} // Setter
}

在 OOP 中这样做是一个很好的做法。

为了处理然后设置数据,可能需要 setters 特殊对象,如数据,具有年、月、日三个子部分。 它们可以被重载以处理基本数据。

维基百科 link 说, 如果日期由单独的私有年月日表示

这不推荐它说如果它存在。永远不要玩弄数据类型,它们的设计是有目的的。

可以在字段声明中使用 uisng 验证器框架进行验证,例如

..
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
...

    public class xyz{


        private int id;

        @NotEmpty(message="Name is compulsary")
        @Size(min = 3, max = 100,message="Name  must be between 3 to 100 characters")
        private String personName;
        @NotEmpty(message="Person type is compulsary")
        private String personType;
        private String designation;
        private String purpos.......

您提供的示例不合适,至少在您提到的形式和名称中不合适。

我会尝试一些更好的例子:

二传手

您可能希望将它们主要用于验证。例如 setDate(Date d) 可以检查数据是否在某个范围内,例如未来不超过 20 年等(取决于您的要求)。

吸气剂

如果它们包含的不仅仅是简单的逻辑,它们可能代表虚拟属性,即没有基础字段但即时计算的属性。

让我们以 getAmount() 为例:可能没有任何字段 amount 或者由于某些原因(例如没有精度问题),金额可能以美分(或更小)存储。因此 getAmount() 可能 看起来像这样:

public double getAmount() {
  return amountInCents / 100.0;
}

请注意,名称 getAmount() 可能会产生误导,因此您最好使用 getAmountInUSD() 或类似的名称。

一般

在大多数情况下,建议在 Java 中使用吸气剂和 setters,因为您可以执行以下操作(列表不完整):

  • 添加验证逻辑(到 setters)
  • 为虚拟属​​性添加转换逻辑(setters,getter)
  • 定义访问权限,即只读意味着没有 public setter
  • 使用基于 Java Beans 规范的库(需要使用 setters 和 getter)
  • 解耦 getter/setter 的 client/caller,即如果在某些时候你想添加通过 setter 完成字段访问的验证将不需要客户端更改(除非需要处理验证错误)等等
  • 使用 setters 和 getter 进行调试,例如通过在方法上放置一个断点并查看堆栈跟踪以查看调用它的人(dsp_user 提到)

这些做法好吗? YesNo。原因如下:

1. setDate 方法将 java.util.Date 日期存储在 3 个单独的私有字段中,例如年、月、日。

如果您密切注意 java.util.Date 的代码,它已被弃用,将日期存储在 3 个单独的字段中。这是它被弃用的原因。

A java.util.Date 有日期和时间部分。您可以忽略代码中的时间部分。在这种情况下,date class 将采用 JVM 默认时区定义的一天的开始,并将该时间应用于 Date 对象。因此,您的代码的结果将根据它运行的机器或设置的时区而有所不同。可能不是你想要的。

所以这样做不是好的做法,Java dev 实现并弃用了某些功能。

2。 getAmount 方法连接 2 个字段数字和货币以及 return 类似“100 美元”的东西。

是的,这非常好,除非并且直到您将此值仅用于显示目的而不是用于引用,因为此值是两个其他值的最终串联。

setDate method stores java.util.Date date in 3 separate private fields like year, month, day

是的,你可以做到。您如何处理日期是您的事,如果您愿意,可以将其存储在字符串中,无论如何都不应该干扰 class.

的用户

验证问题不同。您可以阅读有关 design by contract

的内容

假设你有一个 class 鸿沟:

class Divide {
private int dividend = 0;
private int diviser = 1;
private int result;
private int modulo;

public void  setDividend(int dividend) {
    this.dividend = dividend;
    this.result = this.dividend / this.diviser;
}

public void setDiviser(int diviser) {
    this.diviser = diviser;
    this.result = this.dividend / this.diviser;
}

...

问题是你是否应该检查 diviser 是否为空。这就是合同设计解决的问题。所以你可以这样写:

   public void setDiviser(int diviser) {
        if (diviser == 0) throw ... // Zero divide exception
        this.diviser = diviser;
        this.result = this.dividend / this.diviser;
    }

但这非常低效,因为每次设置分隔符时您都在进行此检查,也许调用者也已检查并且操作完成时将再次检查。

所以:

   // I have been told "by contract" that diviser won't be zero
   public void setDiviser(int diviser) {
        this.diviser = diviser;
        try {
            this.result = this.dividend / this.diviser;
        }
        catch (Exception e)
        {
            // do something about it
        }
    }

还有其他选项,例如使方法可抛出,但如果您想进行有效的验证,请务必小心。当然 Java 不会完全按照合同实施设计,但除了例外情况,您可以保持这种精神。

setters 和 getters

的轶事

我的观点?就像这个世界上的任何东西一样,它不是一个黑白分明的明确界限,什么是使用 getter 和 setters 的最佳方式。


Setterless classes 和 Builder 模式

有时 class 有很多属性(设计一个包含所有属性的构造函数是不切实际的,而且如果你这样做的话,使用起来会非常痛苦)。补充一下,很多时候这样的 classes 在构建后应该是不可变的(所以没有 public setters)。

A Builder pattern with a Fluent interface 得到了东西......好吧......流动

设置时间是否生效?

对于大多数情况,可以在 setters 级别执行验证。然而,这样做并不总是明智的。

例如,在某些情况下,对象可以 "transition" 通过无效状态并仅在 setter 中的一系列调用后达到 "valid and self consistent state"。
例如:"A motorcycle without two wheels is not a valid motorcycle" 并不意味着我不能 setFrontWheel(null) 作为临时阶段 修理我的摩托车...该死的合同设计,我会在最后调用 validateMe 并完成它。

多setters

如果您需要对设置执行验证并且某些值组合没有意义,请使用 multi-setter:

void setDate(int y, int m, int day) {
  int maxDaysInMonth=0;
  switch(m) {
    case 1:
    case 3:
    ...
    case 11:
       maxDaysInMonth=31;
       break;
    case 2: // that's feb
       maxDaysInMonth=isLeap(y) ? 29 : 28;
       break;
    case 4:
    case 6:
    ...
       maxDaysInMonth=30;
       break;
    default: // only 12 months in the year
       throw something;
  }
  if(d>=maxDaysInMonth) {
    // you catch my drift, yeah?
  }
}

吸气剂:

  1. "computed getters" - 就像“100 美元” - 本身不是 属性,许多人会争辩说该方法应该称为 "computeSomething" 或 "toSomeForm"(和"toString"一样),但是...就是这么方便又好记Rectangle.getCentreX

  2. "restricted getters" - 喜欢

    protected ArrayList listeners;
    // look-but-don't-touch getter
    public List getListeners() {
      return Collections.unmodifiableList(this.listeners):
    }
  1. "void returning getters" - 当要在作为参数提供的目的地内返回(复制或放置)值时。当您想要拒绝对数据成员本身的直接访问 并避免创建内部数据的副本时很有用 - 也与高性能要求齐头并进。例如
     class Rectangle {
        void getCentre(Point2D resultHere) {
          resultHere.set(minX+width/2, minY+height/2);
        }
        // and not
        // Point2D getCentre() {
        //   return new Point2D.Double(minX+width/2, minY+height/2);
        // }
        // because... performance.
     }

TL;DR:是的。这正是 setters 和 getters 的意义所在!

首先要理解的是为什么我们首先使用 getters 和 setters。

getters 和 setters 的目的是能够将 class 呈现为 public API,以便用户可以存储它们的对象并以应有的方式操作数据(对于某些 classes,例如不可变的 class,这可能意味着根本不对其进行操作)。

getter 允许我们公开存储在 class 中的信息,而用户不知道这些信息是如何存储的。 phone 号码是存储在号码的每个部分(对于美国号码)的 3 个单独字段中还是存储在单个字段中,这与用户无关。他们只关心以 class 合同

指定的格式获取它

setter 允许用户操作对象。同样,他们不关心当他们传入 Date 对象时,您是否将其拆分为单独的字段。他们只是想知道如果您提供一个 getter 承诺传回等效的 Date 对象,那么他们会得到一个与他们在 setter 中传递的对象等效的 Date 对象。

getter 和 setter 的全部目的是 class 的用户不知道 class 中实际存在哪些字段(这这就是为什么始终将它们设为私有的原因。

为什么将所有字段设为私有如此重要?

通过将所有字段设为私有,我们可以稍后返回并更改我们想要的任何内容,只要它不更改 getter 方法的结果即可。

例如,假设我们有这样一个 class 代表一个人:

public class Person {

    private String name;
    private String address;

    public Person(String firstName, String lastName) {
        setName(firstName, lastName);
    }

    /**
     * @return The first and last name as a single String
     */
    public String getName() {
        return name;
    }

    public void setName(String firstName, String lastName) {
        this.name = firstName + " " + lastName;
    }

    /**
     * @return The entire address as a single String
     */
    public String getAddress() {
        return address;
    }

    public void setAddress(String houseNumber, String streetName, String city, String state, String zip) {
        this.address = houseNumber + " "  + streetName + " " + city + " " + state + " " + zip;
    }
}

在创建 class 时,我们决定不需要单独存储名字和姓氏,地址也可以是单个字段(无论出于何种原因)。如果我们想改变它,将名字和姓氏分成两个单独的字段,地址也应该分成单独的字段,会发生什么。

如果我们让字段暴露(通过使它们 public),我们将永远无法更改它们。任何人都可以通过声明 Person.name = "John Doe" 来使用它们。更改字段名称会完全破坏他们的程序。

如果我们将它们保密,我们现在可以为所欲为。我们只需要更新我们的方法,使它们 return 相同的数据。因此,要拆分名称和地址字段,我们将执行以下操作:

public class Person {

    private String firstName;
    private String lastName;
    private String houseNumber;
    private String streetName;
    private String city;
    private String state;
    private String zip;

    public Person(String firstName, String lastName) {
        setName(firstName, lastName);
    }

    /**
     * @return The first and last name as a single String
     */
    public String getName() {
        return firstName + " " +lastName;
    }

    public void setName(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    /**
     * @return The entire address as a single String
     */
    public String getAddress() {
        return houseNumber + " " + streetName + " " + city + " " + state + " " + zip;
    }

    public void setAddress(String houseNumber, String streetName, String city, String state, String zip) {
        this.houseNumber = houseNumber;
        this.streetName = streetName;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
}

我们只是能够更改 class 的整个实现,而不会破坏任何使用我们代码的人的代码。这就是封装的力量,也是它在 OOP 中如此重要的原因。

如果我们觉得用户应该能够直接操作每个字段,那么我们可以为各个字段添加 getters 和 setters。当然,这样做我们现在无法改回并合并字段,因为有人可能正在使用 getters 和 setters.

验证在哪里适合所有这些?

您也询问了验证。现在应该很清楚,class 的 public 方法定义了 class 合约。验证数据非常重要,因为它通过不允许任何人将错误数据放入该对象来强制执行合同。以后你总是可以放宽限制,添加它们是一件更难的事情,并且总是有可能破坏代码。

例如,如果您的 Person class 中有一个 age 字段。您的 class 中可能有一个 setAge(int age) 方法(更有可能它是根据 birthDate 字段计算的,但现在假设它只是一个 age 字段) .

该方法应如下所示:

public void setAge(int age) {
    if (age < 0 || age > 120) 
        throw IllegalArgumentException("age must be between 0 and 120");

    this.age = age;
}

年龄的上限值得商榷,但必须验证下限!没有人可以小于 0 岁。根据您的应用程序,您可能希望提高下限。也许他们需要年满 13 或 18 岁才能使用您的应用程序。

如上所述,晚加限制比晚放宽困难得多。