@Retention(value=SOURCE) @Target(value=TYPE) public @interface KolobokeMap
@KolobokeMap
-annotated typesMap
model defining method formsThe annotated type must have at least one abstract method (either declared in the body of the type or inherited) matching (see below) some form from the following list:
Return type | Signature | Notes |
---|---|---|
ValueType |
put(KeyType key, ValueType value) | |
ValueType |
putIfAbsent(KeyType key, ValueType value) | |
ValueType |
replace(KeyType key, ValueType value) | |
boolean |
replace(KeyType key, ValueType oldValue, ValueType newValue) | |
void |
justPut(KeyType key, ValueType value) |
Semantically equivalent to put() , except it doesn't return the previous
mapped value |
ValueType |
addValue(KeyType key, ValueType addition) |
Only if the ValueType is a numeric primitive type |
ValueType |
addValue(KeyType key, ValueType addition, ValueType initialValue) |
Only if the ValueType is a numeric primitive type |
KeyType
and ValueType
are either some reference types (reference
type is a declared type, or an array type, or a type variable) or numeric primitive types
(byte
, char
, short
, int
, long
, float
or
double
). KeyType
and ValueType
are independent in this regard: i. e. the
KeyType
could be int
with the ValueType
of String
or V
(a type variable) or long
, and so on, in any combination.
"A method matches a form" means that the form and the method have the same signature
(as defined in the Java Language Specification, ยง8.4.2), and return type of the form is identical
to the return type of the method, or, if the return type of the form is a reference type, it is a
subtype of the return type of the method. If the method is annotated with a @MethodForm
annotation, for signature comparison, instead of the method's own name,
the name specified by the @MethodForm
is taken. The method might declare any exceptions
in the throws
clause. Method parameter names are not included in method
signatures in the Java language, hence they shouldn't necessarily be the same in the from and the
matching method.
KeyType
and ValueType
consistencyKeyType
and ValueType
should be the same in all methods (either abstract or
not) in the @KolobokeMap
-annotated type, which match the above forms (and the forms
defined below). For example, the annotated type couldn't simultaneously have methods int put(K, long)
and String replace(K, String)
, because according to the first method
the ValueType
is int
, and according to the second method the ValueType
is
String
.
Map
model definition)Hereafter in this specification and specifications of other Koloboke Compile annotations, for
some @KolobokeMap
-annotated type, the KeyType
and ValueType
appearing in
the abstract methods of this type, which match the above forms, are called the key type
and the value type of the annotated type.
If either the key type or the value type or both are some numeric primitive types, methods
(either abstract or not) could match additional versions of the put()
, putIfAbsent()
or both replace()
forms from the above list: a version with primitive
wrapper classes in the positions of the corresponding primitive numeric KeyType
and
ValueType
. For example, if the key type of the annotated type is int
and the
value type is long
, it could have method long put(int, long)
, or Long put(Integer, Long)
, or both simultaneously. If the key type of the annotated type is String
and the value type is double
, it could have methods double addValue(String, double)
and boolean replace(String, Double, Double)
. Among the
methods of some @KolobokeMap
-annotated type some method forms could appear in the
"primitive" version, some other forms - only in the "boxed" version, and some forms could appear
in both versions. "Mixed" versions and "boxed" versions of justPut()
and both addValue()
forms are disallowed:
long put(Integer, long)
,
void justPut(String, Double)
.
A numeric primitive key or value type could even never appear as itself in the signatures and
return types of the methods of the annotated type (but only as a wrapper class in methods
matching the "boxed" versions of some forms): for example, a @KolobokeMap
-annotated
interface extending Map<Long, Double>
and not defining own methods (hence inheriting
abstract methods like Double put(Long, Double)
, which match the "boxed" versions of the
defined forms) still have primitive long
key type and primitive double
value
type. This is important, because in the generated implementations for such types keys and/or
values are stored as primitives, hence insertion of the null
key or value into such map
is not possible and always leads to NullPointerException
. The only way to make Koloboke
Compile to generate a map implementation that actually stores primitive wrapper objects (though
this is a highly questionable goal) is to annotate with @KolobokeMap
a generic abstract
class or an interface with key or value type of a type variable, and parameterize it with
primitive wrapper class (e. g. Integer
) at the moment of instantiation.
Currently Koloboke Compile is able to generate only hash table-based implementations of annotated Map-like classes or interfaces. In the future, ability to generate other kinds of implementations (e. g. tree-based) might be added.
In addition to the method forms specified above, a KolobokeMap
-annotated class or
interface could have methods (either abstract or not) matching the following forms:
KeyType | ValueType |
Prototyping interface |
---|---|---|
A reference type | A reference type | HashObjObjMap<KeyType, ValueType> |
A reference type | A numeric primitive type | HashObjYyyMap<KeyType> , where Yyy is a capitalized name of the value
type, e. g. HashObjIntMap<KeyType> if the value type is
int |
A numeric primitive type | A reference type | HashXxxObjMap<ValueType> , where Xxx is a capitalized name of the key
type, e. g. HashLongObjMap<ValueType> if the key type is
long |
A numeric primitive type | A numeric primitive type | HashXxxYyyMap , where Xxx is a capitalized name of the key type and
Yyy is a capitalized name of the value type, e. g. HashLongIntMap if the
key type is long and the value type is int |
@KolobokeMap
-annotated type in the first two columns of in the
same row.
The Koloboke Collections API comes in two different distributions: for Java 6 or 7, and
for Java 8+, and signatures of some methods are different in the same interfaces, provided by
different distributions. For example, the method ObjObjMap.forEach(BiConsumer)
has the parameter type of java.util.function.BiConsumer
in the distribution for Java 8+, and com.koloboke.function.BiConsumer
in the distribution for
Java 6 and 7. Possible method forms have signatures of the methods in that version of the
prototyping interface, which is present in the compilation classpath at the moment of
Koloboke Compile generation.
Forms, defined by methods, which are not abstract in the prototyping interface, couldn't
be matched by abstract methods in the KolobokeMap
-annotated class or interface, but
if there are some non-abstract matching methods, they should be consistent with the key and
value types of the annotated type, as specified above. This
means, for example, that if the project, containing the @KolobokeMap
-annotated type,
depends on JDK 8 and the Koloboke Collections API distribution for Java 6 and 7 (although
Koloboke Compile emits a warning in this case, suggesting to change the dependency to the
Koloboke Collections API distribution for Java 8+), method Map.forEach(java.util.function.BiConsumer)
will have a default implementation (i. e. it will
be non-abstract) in the HashObjObjMap
interface, so the
@KolobokeMap
-annotated type couldn't declare or inherit such a method as abstract.
Since as specified above a form allows a matching
method to have reference return type to be a supertype of the return type of the form, it is
not required from e. g. the keySet()
method to have the return type of HashObjSet<KeyType>
or HashLongSet
(the keySet()
return
types in the HashObjObjMap<KeyType, ValueType>
and HashLongObjMap
interfaces,
respectively). The keySet()
return type could be Set<KeyType>
or
Set<Long>
, that are supertypes of HashObjSet<KeyType>
and HashLongSet
respectively. This is useful, if the @KolobokeMap
-annotated type shouldn't depend on
the Koloboke Collections API.
Map
interface
Return type | Signature and Description |
---|---|
boolean | justRemove(Object key) Semantically equivalent to remove(Object) , except instead of the previously
mapped value it returns true , if the removal was successful (actually changed
the map).
|
boolean |
containsEntry(Object key, Object value) Returns true if remove(Object, Object) invoked with the same
arguments would find the entry and return true . |
Map
interfaceget()
and remove()
in the Map
interface, and
consequently in subinterfaces (including primitive specializations) from the Koloboke
Collections API, have the key and value parameters of Object
type, that is a
double-edged design decision. Along with the forms, prototyped by those methods, similar
forms with generified parameter types are also allowed in
@KolobokeMap
-annotated types:
Return type | Signature | Notes |
---|---|---|
ValueType | get(KeyType key) | |
ValueType |
getOrDefault(KeyType key, ValueType defaultValue) | |
boolean |
containsKey(KeyType key) | |
boolean |
containsValue(ValueType value) | |
boolean |
containsEntry(KeyType key, ValueType value) |
Unless the key and value types are both Object or unbound type
variables |
ValueType |
remove(KeyType key) | |
ValueType |
removeAsYyy(KeyType key) | Only if the key type is a reference type
and the value type is a numeric primitive type. Yyy is a capitalized name of
the value type. |
boolean |
justRemove(KeyType key) |
Only if the key type is not an unbound type variable |
boolean |
remove(KeyType key, ValueType value) |
Koloboke Compile not only permits generified versions of the
"Object
-parameterized" methods, but effectively opts out the feature of making
queries with arguments of unrelated types for Maps with concrete (not a type variable) key or
value type. For example, if a @KolobokeMap
-annotated class has ArrayList
key type, it may have a method get(Object)
, but an attempt to call this method with
LinkedList
argument will result in ClassCastException
, with a message like
"LinkedList couldn't be cast to ArrayList". However, if the key type of a
@KolobokeMap
-annotated class or interface is a type variable, and it is ArrayList
at the instantiation point, map.get(linkedList)
will proceed without
exception and may return some value, if there is a mapping with an equivalent ArrayList
as the key in this map.
The @KolobokeMap
-annotated type could have an abstract method of the form of the
Object.equals(Object)
method, only if it is a subtype of Map
.
The @KolobokeMap
-annotated type could have any static methods and non-abstract
methods with any signatures and return types, as long as they don't match any of the forms
defined above (but if they do match some forms, they should have
consistent signatures and return types).
The annotated type could be a nested type, but in this case it should be static and non-private. The annotated type couldn't be an inner or an anonymous class.
If the annotated type is an abstract class, it must have exactly one non-private constructor (the implicit no-argument constructor counts).
The annotated type couldn't have a @KolobokeSet
annotation (any type could
either have a @KolobokeMap
or a @KolobokeSet
annotation, but not both).
@KolobokeMap
-annotated type is MyMap
, the name of the implementation class is KolobokeMyMap
. If the implemented type is
a nested type, instead of the simple name of the implemented type, it's canonical name (excluding
package) is used with dots replaced with underscores: _
. For example, if a
@KolobokeMap
-annotated type named InnerMap
is a nested type in a type named
Outer
, the name of the Koloboke Compile-generated implementation class is KolobokeOuter_InnerMap
.
The implementation class is located in the same package, as the implemented type, and has package-private visibility in this package.
If the @KolobokeMap
-annotated type has type parameters, the implementation class has
the same number of type parameters with equivalent types in the same order ("equivalence of types
of type parameters" essentially means equivalence of their bounds).
The implementation class is non-abstract. It overrides all abstract methods in the implemented
type, and doesn't override non-abstract methods. Methods in the implementation class, overriding
some methods in the implemented type, have contracts of the methods in interfaces from the
Koloboke Collections API, prototyping the forms, matched by the overridden methods in the
implemented type. If a form of an overridden method is not prototyped by methods in interfaces
from the Koloboke Collections API (e. g. justPut()
and justRemove()
forms, which
are not present in any interfaces from the Koloboke Collections API), contracts of overriding
methods in the implementation class are specified in a "Notes" column in one of the tables above,
where the corresponding form is defined.
The implementation may call any (even not documented as methods forms in this document)
non-abstract methods of the implemented type, which it would otherwise generate itself. To ensure
that user implementations of some methods don't change semantics of Koloboke Compile-generated
methods in some unintended way (because they call underridden methods), it is recommended to
manually check all usages of non-abstract methods of the implemented type in the Koloboke
Compile-generated implementation class. If it appears that behaviour of some methods indeed
become wrong because of cross-dependencies between methods in the implemented and implementation
classes, a possible workaround is to declare new abstract method(s) having the same parameter and
return types as the underridden method(s) which caused undesirable semantics change of other
methods, but with different names, and annotate them with @MethodForm("nameOfTheUnderriddenMethod"
. This makes Koloboke Compile to "remap"
usages of the underridden methods in the generated implementation (usages, which statically
resolve to the underridden methods, i. e. where the type of the method call
receiver is the implemented type or the implementation type, but not a supertype of the
implemented type) to the newly declared abstract methods as well as generate implementations for
these methods.
No guarantee is provided on whether in the generated class such bulk methods as Map.putAll(java.util.Map<? extends K, ? extends V>)
, map view's Collection.containsAll(java.util.Collection<?>)
, Collection.removeAll(java.util.Collection<?>)
, Set.retainAll(java.util.Collection<?>)
, etc. are implemented via single-key query methods such as Map.put(Object, Object)
, Map.containsKey(Object)
, Map.remove(Object)
etc. or
not. More generally, is it not specified how and if any methods of the implemented class, it's
cursors, views, iterators and cursors of the views may call each other.
equals()
and hashCode()
The implementation class overrides Object.equals(Object)
, Object.hashCode()
if
the implemented type is a subtype of Map
and the methods are abstract in the implemented
type. If any of them is non-abstract, but inherited directly from Object
(i. e. inherited
the default implementation), it is still overridden.
toString()
The implementation class overrides Object.toString()
if it is abstract in the implemented
type. If it is non-abstract, but inherited directly from Object
, it is still overridden.
Contracts of the methods in the generated implementation class might be affected by additional annotations applied to the implemented type, see the Implementation Customizations section below.
The implementation class is declared final
(not extensible).
Koloboke Compile "doesn't know" about Serializable
and Cloneable
interfaces:
it doesn't check if @KolobokeMap
-annotated classes or interfaces implement or extend
these interfaces, nor overrides Object.clone()
or implements readObject()
and
writeObject()
methods, specified by the Serializable
interface. The
implementation classes don't declare any fields transient
. However,
@KolobokeMap
-annotated types could intercept by providing implementations for
clone()
or serialization methods itself.
The implementation class is not synchronized in any way. It doesn't internally use the
intrinsic object lock of the receiver instance nor any input objects. (So it doesn't declare any
methods synchronized
.) Abstract @KolobokeMap
-annotated types are free to use the
intrinsic lock (e. g. declare some non-abstract methods synchronized
).
The implementation class has two constructors. One constructor accepts a parameter of
the int
type, the expected size of the map being constructed. Another constructor
accepts two parameters: the first of the HashConfig
type -- a hash configuration for the
constructed map, the second of the int
type, the expected size of the map. Calling the
first constructor is equivalent to calling the second, with the default hash config passed as the first argument.
If the implemented type is an abstract class and it's non-private constructor has some
parameters, parameters lists of both constructors of the implementation type are preceded
with parameters of the same types as the parameters of the implemented type's constructor,
and they are passed over to the implemented class constructor in a super()
call. For
example, if a @KolobokeMap
-annotated abstract class has a non-private constructor with
parameter types int
, String
and Object
, parameter types of the
constructors of the generated class have the following types:
int, String, Object, int (expectedSize)
int, String, Object, HashConfig, int (expectedSize)
If the non-private constructor of the implemented class has type parameters, equal type parameters are present in both constructors in the implementation class as well.
If the @KolobokeMap
-annotated type is annotated with one of the annotations from the
com.koloboke.compile.mutability
package, Koloboke Compile generates an implementation
with the specified mutability profile. If none of the annotations from this package is
applied to a @KolobokeMap
-annotated type, Koloboke Compile generates
an implementation with the least mutability, which supports all the abstract methods in
an implemented type. See the package documentation
for more information.
By default, if a @KolobokeMap
-annotated type has a reference key type, Koloboke
Compile generates an implementation that disallows insertion and querying the null
key,
i. e. calls like map.put(null, value)
and map.get(null)
on instances of such
implementation result in NullPointerException
. Koloboke Compile generates an
implementation that allows the null
key, only if the implemented type is annotated with
@NullKeyAllowed
.
By default Koloboke Compile generates a Map implementation (for a model with a reference
key type) which relies on the Java built-in object equality (Object.equals(Object)
and Object.hashCode()
) for comparing keys, just as Map
interface specifies. But
if the implemented type has two non-abstract methods which match boolean keyEquals(KeyType, KeyType)
int keyHashCode(KeyType)
forms, the generated
implementation class relies on these methods for determining hash codes and comparing the queried
keys. This may be used for "redefining" inefficiently implemented equals()
and hashCode()
for some key type, or making key equivalence configurable, or using a non-standard equivalence relationship in
the keys domain, e. g. defining a IdentityHashMap
-like type with primitive int
values. See the @CustomKeyEquivalence
specification for more
information.
See the documentation for the
com.koloboke.compile.hash.algo.openaddressing
package.
By default Koloboke Compile generates a Map implementation that returns so-called fail-fast
iterators (of the collection views) and cursors, and tries to identify concurrent structural
modifications during bulk operations like forEach()
, removeIf()
, putAll()
, and during bulk operations on the collections views. This is consistent with HashMap
's behaviour. If instances of the Koloboke Compile-generated implementation of the
@KolobokeMap
-annotated type are going to be accessed only via single-key queries (e. g.
only via get()
and put()
), disabling concurrent modification checks by applying
@ConcurrentModificationUnchecked
might improve the
implementation performance a bit.
The Map
interface returns null
from get()
method if no value mapped
for the key is found, from put()
, putIfAbsent()
, replace()
and remove()
, if the mapping for the key didn't exist before the operation, and passes null
as the argument for the lambda in compute()
, if the mapping for the key didn't exist
before the compute operation. In primitive specializations of these methods using null
for such "no-value" purposes is not possible, because null
is not a valid primitive
value. In the Koloboke Collections API, the concept of default value is introduced to
address this problem. In the Koloboke Collections API, the default value of the map is
defaultValue()
method, e. g. ObjIntMap.defaultValue()
withDefaultValue()
method, e. g. ObjIntMapFactory.withDefaultValue(int)
ValueType addValue(KeyType key, ValueType addition)
method, the version of addValue()
without initialValue
provided.
By default in the Koloboke Collections implementation library and Koloboke Compile-generated
implementations the default value is zero for all numeric primitive value types, i. e. 0
for int
, 0.0f
for float
, etc.
If the @KolobokeMap
-annotated type has a non-abstract, non-private method of the
ValueType defaultValue()
signature, the Koloboke Compile-generated implementation calls
this method whenever it needs to return "no value" or pass "no value" to the lambda argument of
the compute()
method. Example:
@KolobokeMap
abstract class MinusOneDefaultValueObjIntMap<K> implements ObjIntMap<K> {
static <K> ObjIntMap<K> withExpectedSize(int expectedSize) {
return new KolobokeMinusOneDefaultValueObjIntMap<K>(expectedSize);
}
@Override
public final int defaultValue() {
return -1;
}
}