Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any way to access this.toString()'s value when calling another constructor?

For everyone who is talking about the fact that the object is in an "unitialized state", please refer to the answer to this question which shows that an object reference can be passed around, dereferenced, have methods invoked from it, and have fields accessed before a constructor terminates and all fields have been assigned (including final fields).

So here's the use case:

public class Entity {

    private final String name;

    public Entity() {
        this(toString()); //Nope, Chuck Testa
    }

    public Entity(String name) {
        this.name = name;
    }
}

The compiler error is:

Cannot refer to an instance method while explicitly invoking a constructor.

Note that toString() has not been overriden and is the default call from Object.

I'm certainly interested in the philosophical/technical reasons behind this, so if anyone can explain that, that would be an awesome bonus. But I'm looking for a way to call toString() from that default constructor as it refers down to the more specific one with more arguments. The actual use case is a bit more complicated and ends up referring all the way down to a constructor with four arguments, but that shouldn't really matter.

I know I could do something like this...

private static final String TO_STRING_CONSTRUCTOR_ARGUMENT = "aflhsdlkfjlkswf";

public Entity() {
    this(TO_STRING_CONSTRUCTOR_ARGUMENT);
}

public Entity(String name) {
    this.name = name == TO_STRING_CONSTRUCTOR_ARGUMENT ? toString() : name;
}

... but it seems like a pretty inelegant solution.

So, any way to pull it off? Or any recommended best practices to deal with this situation?

like image 995
asteri Avatar asked Oct 02 '22 17:10

asteri


3 Answers

I would prefer not to pass this around until the object is created. Instead I would do this:

public class Entity {

    private final String name;

    public Entity() {
        this(null); // or whatever
    }

    public Entity(String name) {
        this.name = name;
    }

    public String getName() { 
        return name != null ? name : Objects.hashCode(this);
    }
}

If you can live without the final name, you can use an initializer block:

public class Entity {

    private String name;

    {name = this.toString();}

    public Entity() {
    }

    public Entity(String name) {
        this.name = name;
    }
}

this is only available after all calls to this() or super() are done. The initializer runs first after the constructors call to super() and is allowed to access this.

like image 60
atamanroman Avatar answered Oct 18 '22 11:10

atamanroman


As for the reasons why that is a compiler error, please see section 8.8.7 of the JLS. The reasons why this was made a compiler error are not clear, but consider that the constructor chain has to be the first thing executed when new'ing an Object and look at the order of evaluation here:

public Entity() {
        this(toString()); 
}

toString() is evaluated first before the even the super constructor is invoked. In general this leaves open all kinds of possibilities for uninitialized state.

As a personal preference, I would suggest that everything an object needs to have in order to create valid state should be available within its constructor. If you have no way of providing valid state in a default constructor without invoking other methods defined in the object hierarchy, then get rid of the default constructor and put the onus on the users of your class to supply a valid String to your other constructor.

If you are ultimately just trying invoke the other constructor with the value of toString(), then I would suggest the following instead:

    public Entity() {
        name = toString();
    }

which accomplishes the same goal you set out to achieve and properly initializes name.

like image 24
whaley Avatar answered Oct 18 '22 12:10

whaley


As explained in the JLS this is not allowed before the instance is initialized.

However, there are ways to handle your scenario in a consistent manner.

As I see your case, you want to signify either a generated value (toString()) or a user provided value, which can be null.

Given this constraints, using TO_STRING_CONSTRUCTOR_ARGUMENT is failing for at least one specific use case, however obscure it may be.

Essentially you will need to replace the String with an Optional similar to what exists in Google Guava and will be included in Java 8, and seen in many other languages.

Having a StringOptional/StringHolder or whatever you choose, similar to this:

public class StringOptional {
  private String value;
  private boolean set = false;
  public StringOptional() {}
  public StringOptional(String value) {
    this.value = value;
    this.set = true;
  }

  public boolean isSet() { return set; }
  public String getValue() { return value; }
}

Then you can call constructors with the knowledge of the inferred path.

public class Entity {
   public Entity() { 
     this(New StringOptional());
   }

   public Entity(String s) {
     this(new StringOptional(s));
   }

   private Entity(StringOptional optional) {
     super(optional);
   }
}

And store this for subsquent need:

if (optional.isSet() ? optional.getValue() : toString();

This is how I usually would handle a maybe-null scenario, hope it augments as an answer.

like image 32
Niels Bech Nielsen Avatar answered Oct 18 '22 12:10

Niels Bech Nielsen